From 92a62e16245cd8acf21d94dc2b7b0a694d014973 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:44:31 +0000 Subject: [PATCH 1/5] feat(bootstrap): add friendly macos defaults --- docs/bootstrap.md | 18 +- docs/bootstrap/macos-defaults.md | 77 ++++- docs/tips-and-tricks.md | 8 +- e2e/cli/test_system_defaults | 30 ++ src/config/config_file/mise_toml.rs | 33 ++ src/system/mod.rs | 491 +++++++++++++++++++++++++++- 6 files changed, 649 insertions(+), 8 deletions(-) diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 9f90bd59ce..5c8d780663 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -44,8 +44,23 @@ task runs every time, so keep it idempotent. "~/.config/nvim" = { mode = "symlink" } "~/.zshrc/activate" = { block = 'eval "$(mise activate zsh)"' } +[bootstrap.macos.dock] +autohide = true +orientation = "left" +tilesize = 48 + +[bootstrap.macos.finder] +show_pathbar = true + +[bootstrap.macos.keyboard] +key_repeat = 2 +initial_key_repeat = 15 + +[bootstrap.macos.trackpad] +tap_to_click = true + [bootstrap.macos.defaults] -"com.apple.dock" = { autohide = true } +"com.apple.finder" = { AppleShowAllFiles = true } [bootstrap.macos.launchd.agents.my-sync] program = "~/.local/bin/my-sync" @@ -106,6 +121,7 @@ place but should not install anything during that check. | ---------------------------------- | ------------------------------------------------------------- | | `[bootstrap.packages]` | OS packages from apt, dnf, pacman, or brew | | `[dotfiles]` | Whole-file dotfiles and small managed edits to existing files | +| `[bootstrap.macos.*]` | Curated macOS preferences for Dock/Finder/keyboard/trackpad | | `[bootstrap.macos.defaults]` | macOS user preferences written through `defaults write` | | `[bootstrap.macos.launchd.agents]` | macOS user LaunchAgents written and loaded with `launchctl` | | `[bootstrap.user]` | Current-user settings such as `login_shell` | diff --git a/docs/bootstrap/macos-defaults.md b/docs/bootstrap/macos-defaults.md index c9698be1a1..6d2f8f2d44 100644 --- a/docs/bootstrap/macos-defaults.md +++ b/docs/bootstrap/macos-defaults.md @@ -5,12 +5,83 @@ mise can declare macOS user defaults (preferences) in the `mise bootstrap macos-defaults apply`: ```toml +[bootstrap.macos.dock] +autohide = true +orientation = "left" +tilesize = 48 +show_recents = false + +[bootstrap.macos.finder] +show_all_files = true +show_pathbar = true +preferred_view_style = "list" + +[bootstrap.macos.keyboard] +key_repeat = 2 +initial_key_repeat = 15 +press_and_hold = false + +[bootstrap.macos.trackpad] +tap_to_click = true + [bootstrap.macos.defaults] -NSGlobalDomain = { KeyRepeat = 2, InitialKeyRepeat = 15, ApplePressAndHoldEnabled = false } -"com.apple.dock" = { autohide = true, tilesize = 48, orientation = "left" } -"com.apple.finder" = { ShowPathbar = true, AppleShowAllFiles = true } +"com.apple.finder" = { AppleShowAllFiles = true } ``` +The curated sections compile to raw defaults entries. Use +`[bootstrap.macos.defaults]` for preferences not covered by the friendly +sections, or to override the raw `(domain, key)` generated by a friendly +setting. + +## Friendly sections + +`[bootstrap.macos.dock]` supports: + +| Key | Raw default | +| --------------- | --------------------------------- | +| `autohide` | `com.apple.dock.autohide` | +| `orientation` | `com.apple.dock.orientation` | +| `tilesize` | `com.apple.dock.tilesize` | +| `magnification` | `com.apple.dock.magnification` | +| `largesize` | `com.apple.dock.largesize` | +| `show_recents` | `com.apple.dock.show-recents` | +| `mru_spaces` | `com.apple.dock.mru-spaces` | + +`orientation` must be `bottom`, `left`, or `right`. + +`[bootstrap.macos.finder]` supports: + +| Key | Raw default | +| ------------------------- | ------------------------------------------------ | +| `show_all_files` | `com.apple.finder.AppleShowAllFiles` | +| `show_pathbar` | `com.apple.finder.ShowPathbar` | +| `show_status_bar` | `com.apple.finder.ShowStatusBar` | +| `show_extensions_warning` | `com.apple.finder.FXEnableExtensionChangeWarning` | +| `preferred_view_style` | `com.apple.finder.FXPreferredViewStyle` | + +`preferred_view_style` must be `icon`, `list`, `column`, or `gallery`. + +`[bootstrap.macos.keyboard]` supports: + +| Key | Raw default | +| -------------------- | ------------------------------------------------ | +| `key_repeat` | `NSGlobalDomain.KeyRepeat` | +| `initial_key_repeat` | `NSGlobalDomain.InitialKeyRepeat` | +| `press_and_hold` | `NSGlobalDomain.ApplePressAndHoldEnabled` | +| `fn_state` | `NSGlobalDomain.com.apple.keyboard.fnState` | + +`[bootstrap.macos.trackpad]` supports: + +| Key | Raw defaults | +| ------------------- | ------------ | +| `tap_to_click` | `com.apple.AppleMultitouchTrackpad.Clicking`, `com.apple.driver.AppleBluetoothMultitouch.trackpad.Clicking` | +| `three_finger_drag` | `com.apple.AppleMultitouchTrackpad.TrackpadThreeFingerDrag`, `com.apple.driver.AppleBluetoothMultitouch.trackpad.TrackpadThreeFingerDrag` | + +Unknown friendly keys, invalid enum values, and unsupported value types warn +and are ignored. + +## Raw defaults + Each key under `[bootstrap.macos.defaults]` is a preferences domain. Quote domains containing dots. Values map to the matching `defaults write` type: diff --git a/docs/tips-and-tricks.md b/docs/tips-and-tricks.md index 9137edfcef..e0a8e2ebc4 100644 --- a/docs/tips-and-tricks.md +++ b/docs/tips-and-tricks.md @@ -92,8 +92,12 @@ defaults, then LaunchAgents, then login shell, then tools, then a "~/.config/nvim" = { mode = "symlink" } "~/.zshrc/activate" = { block = 'eval "$(mise activate zsh)"' } -[bootstrap.macos.defaults] # macOS defaults write -"com.apple.dock" = { autohide = true } +[bootstrap.macos.dock] # friendly macOS defaults +autohide = true +orientation = "left" + +[bootstrap.macos.finder] +show_pathbar = true [bootstrap.macos.launchd.agents.my-sync] # macOS user LaunchAgents program = "~/.local/bin/my-sync" diff --git a/e2e/cli/test_system_defaults b/e2e/cli/test_system_defaults index 437a64ea33..9cb229b060 100644 --- a/e2e/cli/test_system_defaults +++ b/e2e/cli/test_system_defaults @@ -4,11 +4,28 @@ cat <mise.toml [bootstrap.macos.defaults] NSGlobalDomain = { KeyRepeat = 2 } "com.apple.dock" = { autohide = true, tilesize = 48, orientation = "left" } + +[bootstrap.macos.dock] +show_recents = false + +[bootstrap.macos.finder] +show_all_files = true +preferred_view_style = "list" + +[bootstrap.macos.keyboard] +initial_key_repeat = 15 + +[bootstrap.macos.trackpad] +tap_to_click = true EOF # status renders on any platform; on non-macOS entries are skipped, not errors assert_succeed "mise bootstrap macos-defaults status" assert_contains "mise bootstrap macos-defaults status" "com.apple.dock" +assert_contains "mise bootstrap macos-defaults status" "show-recents" +assert_contains "mise bootstrap macos-defaults status" "AppleShowAllFiles" +assert_contains "mise bootstrap macos-defaults status" "InitialKeyRepeat" +assert_contains "mise bootstrap macos-defaults status" "Clicking" assert_contains "mise bootstrap macos-defaults status --json" '"macos_defaults"' if [[ $(uname) != "Darwin" ]]; then assert_contains "mise bootstrap macos-defaults status" "skipped" @@ -38,6 +55,19 @@ EOF assert_succeed "mise bootstrap macos-defaults status" assert_contains "mise bootstrap macos-defaults status 2>&1" "expected a table" +# friendly defaults validate keys and enum values +cat <mise.toml +[bootstrap.macos.dock] +orientation = "top" +future_key = true + +[bootstrap.macos.finder] +preferred_view_style = "coverflow" +EOF +assert_succeed "mise bootstrap macos-defaults status" +assert_contains "mise bootstrap macos-defaults status 2>&1" "invalid value" +assert_contains "mise bootstrap macos-defaults status 2>&1" "unknown key" + # empty [bootstrap.macos.defaults] section cat <mise.toml [bootstrap.macos.defaults] diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index 40bdf8248e..27e363f1f1 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -2277,6 +2277,19 @@ mod tests { [bootstrap.macos.defaults] NSGlobalDomain = { KeyRepeat = 2, ApplePressAndHoldEnabled = false } "com.apple.dock" = { autohide = true, tilesize = 48, magnification-scale = 1.5, orientation = "left", future-array = [1, 2] } + + [bootstrap.macos.dock] + show_recents = false + + [bootstrap.macos.finder] + show_all_files = true + preferred_view_style = "list" + + [bootstrap.macos.keyboard] + initial_key_repeat = 15 + + [bootstrap.macos.trackpad] + tap_to_click = true "#, ) .unwrap(); @@ -2300,6 +2313,26 @@ mod tests { &toml::Value::String("left".into()) ); assert!(dock.get("future-array").unwrap().is_array()); + assert_eq!( + system.macos.dock.get("show_recents").unwrap(), + &toml::Value::Boolean(false) + ); + assert_eq!( + system.macos.finder.get("show_all_files").unwrap(), + &toml::Value::Boolean(true) + ); + assert_eq!( + system.macos.finder.get("preferred_view_style").unwrap(), + &toml::Value::String("list".into()) + ); + assert_eq!( + system.macos.keyboard.get("initial_key_repeat").unwrap(), + &toml::Value::Integer(15) + ); + assert_eq!( + system.macos.trackpad.get("tap_to_click").unwrap(), + &toml::Value::Boolean(true) + ); file::remove_file(&p).unwrap(); } diff --git a/src/system/mod.rs b/src/system/mod.rs index e7da79d53b..04fe7b4ede 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -62,6 +62,18 @@ pub struct BootstrapUserTomlConfig { #[derive(Debug, Default, Clone, Deserialize)] pub struct BootstrapMacosTomlConfig { + /// Friendly Dock settings that compile into `[bootstrap.macos.defaults]`. + #[serde(default)] + pub dock: IndexMap, + /// Friendly Finder settings that compile into `[bootstrap.macos.defaults]`. + #[serde(default)] + pub finder: IndexMap, + /// Friendly keyboard settings that compile into `[bootstrap.macos.defaults]`. + #[serde(default)] + pub keyboard: IndexMap, + /// Friendly trackpad settings that compile into `[bootstrap.macos.defaults]`. + #[serde(default)] + pub trackpad: IndexMap, /// `[bootstrap.macos.defaults.]` -> key -> value. Values stay raw TOML so /// shapes from newer mise versions (arrays, dicts) parse fine on older /// ones; the domain level is also raw so a malformed section warns @@ -241,15 +253,17 @@ fn packages_from_config_files_with_brew_taps( /// the value a global config declared. Unsupported value shapes warn /// (forward compatibility) and are skipped. pub fn defaults_from_config(config: &Config) -> Vec { - let mut merged: IndexMap<(String, String), toml::Value> = IndexMap::new(); + let mut friendly: IndexMap<(String, String), toml::Value> = IndexMap::new(); + let mut raw: IndexMap<(String, String), toml::Value> = IndexMap::new(); // config_files is ordered local -> global; reverse for global -> local for cf in config.config_files.values().rev() { if let Some(sys) = cf.bootstrap_config() { + merge_friendly_macos_defaults(&mut friendly, &sys.macos); for (domain, entries) in sys.macos.defaults { match entries { toml::Value::Table(entries) => { for (key, value) in entries { - merged.insert((domain.clone(), key), value); + raw.insert((domain.clone(), key), value); } } _ => warn!( @@ -259,6 +273,7 @@ pub fn defaults_from_config(config: &Config) -> Vec { } } } + let merged = merge_raw_over_friendly_macos_defaults(friendly, raw); let mut out = vec![]; for ((domain, key), value) in merged { match DefaultsValue::from_toml(&value) { @@ -297,6 +312,299 @@ pub fn launchd_from_config(config: &Config) -> Vec { out } +fn merge_raw_over_friendly_macos_defaults( + mut friendly: IndexMap<(String, String), toml::Value>, + raw: IndexMap<(String, String), toml::Value>, +) -> IndexMap<(String, String), toml::Value> { + for (key, value) in raw { + friendly.insert(key, value); + } + friendly +} + +fn merge_friendly_macos_defaults( + out: &mut IndexMap<(String, String), toml::Value>, + macos: &BootstrapMacosTomlConfig, +) { + merge_dock_defaults(out, &macos.dock); + merge_finder_defaults(out, &macos.finder); + merge_keyboard_defaults(out, &macos.keyboard); + merge_trackpad_defaults(out, &macos.trackpad); +} + +fn insert_friendly_default( + out: &mut IndexMap<(String, String), toml::Value>, + section: &str, + key: &str, + domain: &str, + defaults_key: &str, + value: toml::Value, + expected: fn(&toml::Value) -> bool, +) { + if expected(&value) { + out.insert((domain.to_string(), defaults_key.to_string()), value); + } else { + warn!( + "[bootstrap.macos.{section}].{key}: unsupported value type \ + (expected bool, integer, float, or string)" + ); + } +} + +fn is_bool(value: &toml::Value) -> bool { + matches!(value, toml::Value::Boolean(_)) +} + +fn is_integer(value: &toml::Value) -> bool { + matches!(value, toml::Value::Integer(_)) +} + +fn merge_dock_defaults( + out: &mut IndexMap<(String, String), toml::Value>, + entries: &IndexMap, +) { + for (key, value) in entries { + match key.as_str() { + "autohide" => insert_friendly_default( + out, + "dock", + key, + "com.apple.dock", + "autohide", + value.clone(), + is_bool, + ), + "orientation" => match value { + toml::Value::String(s) if matches!(s.as_str(), "bottom" | "left" | "right") => { + out.insert( + ("com.apple.dock".to_string(), "orientation".to_string()), + value.clone(), + ); + } + toml::Value::String(_) => warn!( + "[bootstrap.macos.dock].orientation: invalid value \ + (expected bottom, left, or right)" + ), + _ => warn!( + "[bootstrap.macos.dock].orientation: unsupported value type (expected string)" + ), + }, + "tilesize" => insert_friendly_default( + out, + "dock", + key, + "com.apple.dock", + "tilesize", + value.clone(), + is_integer, + ), + "magnification" => insert_friendly_default( + out, + "dock", + key, + "com.apple.dock", + "magnification", + value.clone(), + is_bool, + ), + "largesize" => insert_friendly_default( + out, + "dock", + key, + "com.apple.dock", + "largesize", + value.clone(), + is_integer, + ), + "show_recents" => insert_friendly_default( + out, + "dock", + key, + "com.apple.dock", + "show-recents", + value.clone(), + is_bool, + ), + "mru_spaces" => insert_friendly_default( + out, + "dock", + key, + "com.apple.dock", + "mru-spaces", + value.clone(), + is_bool, + ), + _ => warn!("[bootstrap.macos.dock].{key}: unknown key, ignoring entry"), + } + } +} + +fn merge_finder_defaults( + out: &mut IndexMap<(String, String), toml::Value>, + entries: &IndexMap, +) { + for (key, value) in entries { + match key.as_str() { + "show_all_files" => insert_friendly_default( + out, + "finder", + key, + "com.apple.finder", + "AppleShowAllFiles", + value.clone(), + is_bool, + ), + "show_pathbar" => insert_friendly_default( + out, + "finder", + key, + "com.apple.finder", + "ShowPathbar", + value.clone(), + is_bool, + ), + "show_status_bar" => insert_friendly_default( + out, + "finder", + key, + "com.apple.finder", + "ShowStatusBar", + value.clone(), + is_bool, + ), + "show_extensions_warning" => insert_friendly_default( + out, + "finder", + key, + "com.apple.finder", + "FXEnableExtensionChangeWarning", + value.clone(), + is_bool, + ), + "preferred_view_style" => match value { + toml::Value::String(s) => { + let mapped = match s.as_str() { + "icon" => Some("icnv"), + "list" => Some("Nlsv"), + "column" => Some("clmv"), + "gallery" => Some("glyv"), + _ => None, + }; + if let Some(mapped) = mapped { + out.insert( + ( + "com.apple.finder".to_string(), + "FXPreferredViewStyle".to_string(), + ), + toml::Value::String(mapped.to_string()), + ); + } else { + warn!( + "[bootstrap.macos.finder].preferred_view_style: invalid value \ + (expected icon, list, column, or gallery)" + ); + } + } + _ => warn!( + "[bootstrap.macos.finder].preferred_view_style: unsupported value type \ + (expected string)" + ), + }, + _ => warn!("[bootstrap.macos.finder].{key}: unknown key, ignoring entry"), + } + } +} + +fn merge_keyboard_defaults( + out: &mut IndexMap<(String, String), toml::Value>, + entries: &IndexMap, +) { + for (key, value) in entries { + match key.as_str() { + "key_repeat" => insert_friendly_default( + out, + "keyboard", + key, + "NSGlobalDomain", + "KeyRepeat", + value.clone(), + is_integer, + ), + "initial_key_repeat" => insert_friendly_default( + out, + "keyboard", + key, + "NSGlobalDomain", + "InitialKeyRepeat", + value.clone(), + is_integer, + ), + "press_and_hold" => insert_friendly_default( + out, + "keyboard", + key, + "NSGlobalDomain", + "ApplePressAndHoldEnabled", + value.clone(), + is_bool, + ), + "fn_state" => insert_friendly_default( + out, + "keyboard", + key, + "NSGlobalDomain", + "com.apple.keyboard.fnState", + value.clone(), + is_bool, + ), + _ => warn!("[bootstrap.macos.keyboard].{key}: unknown key, ignoring entry"), + } + } +} + +fn merge_trackpad_defaults( + out: &mut IndexMap<(String, String), toml::Value>, + entries: &IndexMap, +) { + for (key, value) in entries { + match key.as_str() { + "tap_to_click" => { + for domain in [ + "com.apple.AppleMultitouchTrackpad", + "com.apple.driver.AppleBluetoothMultitouch.trackpad", + ] { + insert_friendly_default( + out, + "trackpad", + key, + domain, + "Clicking", + value.clone(), + is_bool, + ); + } + } + "three_finger_drag" => { + for domain in [ + "com.apple.AppleMultitouchTrackpad", + "com.apple.driver.AppleBluetoothMultitouch.trackpad", + ] { + insert_friendly_default( + out, + "trackpad", + key, + domain, + "TrackpadThreeFingerDrag", + value.clone(), + is_bool, + ); + } + } + _ => warn!("[bootstrap.macos.trackpad].{key}: unknown key, ignoring entry"), + } + } +} + /// Desired login shell from the most local config that declares it. pub fn login_shell_from_config(config: &Config) -> Option { let mut shell = None; @@ -454,6 +762,10 @@ fn resolve_managers( mod tests { use super::*; + fn tv(s: &str) -> toml::Value { + s.parse().unwrap() + } + #[test] fn test_parse_use_spec() { let (mgr, req) = parse_use_spec("apt:curl").unwrap(); @@ -502,4 +814,179 @@ mod tests { assert_eq!(brew_tap_name("jq"), None); assert_eq!(brew_tap_name("too/many/slashes/here"), None); } + + #[test] + fn test_friendly_macos_defaults() { + let mut macos = BootstrapMacosTomlConfig::default(); + macos.dock.insert("autohide".into(), tv("true")); + macos.dock.insert("orientation".into(), tv(r#""left""#)); + macos.dock.insert("tilesize".into(), tv("48")); + macos.dock.insert("magnification".into(), tv("true")); + macos.dock.insert("largesize".into(), tv("96")); + macos.dock.insert("show_recents".into(), tv("false")); + macos.dock.insert("mru_spaces".into(), tv("false")); + macos.finder.insert("show_all_files".into(), tv("true")); + macos.finder.insert("show_pathbar".into(), tv("true")); + macos.finder.insert("show_status_bar".into(), tv("true")); + macos + .finder + .insert("show_extensions_warning".into(), tv("false")); + macos + .finder + .insert("preferred_view_style".into(), tv(r#""list""#)); + macos.keyboard.insert("key_repeat".into(), tv("2")); + macos.keyboard.insert("initial_key_repeat".into(), tv("15")); + macos.keyboard.insert("press_and_hold".into(), tv("false")); + macos.keyboard.insert("fn_state".into(), tv("true")); + macos.trackpad.insert("tap_to_click".into(), tv("true")); + macos + .trackpad + .insert("three_finger_drag".into(), tv("true")); + + let mut out = IndexMap::new(); + merge_friendly_macos_defaults(&mut out, &macos); + + assert_eq!( + out.get(&("com.apple.dock".into(), "autohide".into())), + Some(&tv("true")) + ); + assert_eq!( + out.get(&("com.apple.dock".into(), "orientation".into())), + Some(&tv(r#""left""#)) + ); + assert_eq!( + out.get(&("com.apple.dock".into(), "tilesize".into())), + Some(&tv("48")) + ); + assert_eq!( + out.get(&("com.apple.dock".into(), "magnification".into())), + Some(&tv("true")) + ); + assert_eq!( + out.get(&("com.apple.dock".into(), "largesize".into())), + Some(&tv("96")) + ); + assert_eq!( + out.get(&("com.apple.dock".into(), "show-recents".into())), + Some(&tv("false")) + ); + assert_eq!( + out.get(&("com.apple.dock".into(), "mru-spaces".into())), + Some(&tv("false")) + ); + assert_eq!( + out.get(&("com.apple.finder".into(), "AppleShowAllFiles".into())), + Some(&tv("true")) + ); + assert_eq!( + out.get(&("com.apple.finder".into(), "ShowPathbar".into())), + Some(&tv("true")) + ); + assert_eq!( + out.get(&("com.apple.finder".into(), "ShowStatusBar".into())), + Some(&tv("true")) + ); + assert_eq!( + out.get(&( + "com.apple.finder".into(), + "FXEnableExtensionChangeWarning".into() + )), + Some(&tv("false")) + ); + assert_eq!( + out.get(&("com.apple.finder".into(), "FXPreferredViewStyle".into())), + Some(&tv(r#""Nlsv""#)) + ); + assert_eq!( + out.get(&("NSGlobalDomain".into(), "KeyRepeat".into())), + Some(&tv("2")) + ); + assert_eq!( + out.get(&("NSGlobalDomain".into(), "InitialKeyRepeat".into())), + Some(&tv("15")) + ); + assert_eq!( + out.get(&("NSGlobalDomain".into(), "ApplePressAndHoldEnabled".into())), + Some(&tv("false")) + ); + assert_eq!( + out.get(&("NSGlobalDomain".into(), "com.apple.keyboard.fnState".into())), + Some(&tv("true")) + ); + assert_eq!( + out.get(&( + "com.apple.AppleMultitouchTrackpad".into(), + "Clicking".into() + )), + Some(&tv("true")) + ); + assert_eq!( + out.get(&( + "com.apple.driver.AppleBluetoothMultitouch.trackpad".into(), + "Clicking".into() + )), + Some(&tv("true")) + ); + assert_eq!( + out.get(&( + "com.apple.AppleMultitouchTrackpad".into(), + "TrackpadThreeFingerDrag".into() + )), + Some(&tv("true")) + ); + assert_eq!( + out.get(&( + "com.apple.driver.AppleBluetoothMultitouch.trackpad".into(), + "TrackpadThreeFingerDrag".into() + )), + Some(&tv("true")) + ); + } + + #[test] + fn test_friendly_macos_defaults_validation() { + let mut macos = BootstrapMacosTomlConfig::default(); + macos.dock.insert("orientation".into(), tv(r#""top""#)); + macos.dock.insert("tilesize".into(), tv("true")); + macos.dock.insert("unknown".into(), tv("true")); + macos + .finder + .insert("preferred_view_style".into(), tv(r#""coverflow""#)); + macos.keyboard.insert("key_repeat".into(), tv(r#""fast""#)); + macos.trackpad.insert("tap_to_click".into(), tv("[true]")); + + let mut out = IndexMap::new(); + merge_friendly_macos_defaults(&mut out, &macos); + + assert!(out.is_empty()); + } + + #[test] + fn test_raw_macos_defaults_override_friendly_defaults() { + let mut friendly = IndexMap::new(); + friendly.insert( + ("com.apple.dock".into(), "autohide".into()), + toml::Value::Boolean(true), + ); + friendly.insert( + ("com.apple.dock".into(), "tilesize".into()), + toml::Value::Integer(48), + ); + let mut raw = IndexMap::new(); + raw.insert( + ("com.apple.dock".into(), "autohide".into()), + toml::Value::Boolean(false), + ); + + let merged = merge_raw_over_friendly_macos_defaults(friendly, raw); + + assert_eq!( + merged.get(&("com.apple.dock".into(), "autohide".into())), + Some(&toml::Value::Boolean(false)) + ); + assert_eq!( + merged.get(&("com.apple.dock".into(), "tilesize".into())), + Some(&toml::Value::Integer(48)) + ); + } } From 965e1be3075e6eeb3f3e87b92fa484440db1d2c5 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:57:45 +0000 Subject: [PATCH 2/5] fix(bootstrap): preserve macos defaults precedence --- docs/bootstrap/macos-defaults.md | 48 +++++------ src/system/mod.rs | 131 +++++++++++++++++++++++-------- 2 files changed, 123 insertions(+), 56 deletions(-) diff --git a/docs/bootstrap/macos-defaults.md b/docs/bootstrap/macos-defaults.md index 6d2f8f2d44..f383c0b462 100644 --- a/docs/bootstrap/macos-defaults.md +++ b/docs/bootstrap/macos-defaults.md @@ -37,44 +37,44 @@ setting. `[bootstrap.macos.dock]` supports: -| Key | Raw default | -| --------------- | --------------------------------- | -| `autohide` | `com.apple.dock.autohide` | -| `orientation` | `com.apple.dock.orientation` | -| `tilesize` | `com.apple.dock.tilesize` | -| `magnification` | `com.apple.dock.magnification` | -| `largesize` | `com.apple.dock.largesize` | -| `show_recents` | `com.apple.dock.show-recents` | -| `mru_spaces` | `com.apple.dock.mru-spaces` | +| Key | Raw default | +| --------------- | ------------------------------ | +| `autohide` | `com.apple.dock.autohide` | +| `orientation` | `com.apple.dock.orientation` | +| `tilesize` | `com.apple.dock.tilesize` | +| `magnification` | `com.apple.dock.magnification` | +| `largesize` | `com.apple.dock.largesize` | +| `show_recents` | `com.apple.dock.show-recents` | +| `mru_spaces` | `com.apple.dock.mru-spaces` | `orientation` must be `bottom`, `left`, or `right`. `[bootstrap.macos.finder]` supports: -| Key | Raw default | -| ------------------------- | ------------------------------------------------ | -| `show_all_files` | `com.apple.finder.AppleShowAllFiles` | -| `show_pathbar` | `com.apple.finder.ShowPathbar` | -| `show_status_bar` | `com.apple.finder.ShowStatusBar` | +| Key | Raw default | +| ------------------------- | ------------------------------------------------- | +| `show_all_files` | `com.apple.finder.AppleShowAllFiles` | +| `show_pathbar` | `com.apple.finder.ShowPathbar` | +| `show_status_bar` | `com.apple.finder.ShowStatusBar` | | `show_extensions_warning` | `com.apple.finder.FXEnableExtensionChangeWarning` | -| `preferred_view_style` | `com.apple.finder.FXPreferredViewStyle` | +| `preferred_view_style` | `com.apple.finder.FXPreferredViewStyle` | `preferred_view_style` must be `icon`, `list`, `column`, or `gallery`. `[bootstrap.macos.keyboard]` supports: -| Key | Raw default | -| -------------------- | ------------------------------------------------ | -| `key_repeat` | `NSGlobalDomain.KeyRepeat` | -| `initial_key_repeat` | `NSGlobalDomain.InitialKeyRepeat` | -| `press_and_hold` | `NSGlobalDomain.ApplePressAndHoldEnabled` | -| `fn_state` | `NSGlobalDomain.com.apple.keyboard.fnState` | +| Key | Raw default | +| -------------------- | ------------------------------------------- | +| `key_repeat` | `NSGlobalDomain.KeyRepeat` | +| `initial_key_repeat` | `NSGlobalDomain.InitialKeyRepeat` | +| `press_and_hold` | `NSGlobalDomain.ApplePressAndHoldEnabled` | +| `fn_state` | `NSGlobalDomain.com.apple.keyboard.fnState` | `[bootstrap.macos.trackpad]` supports: -| Key | Raw defaults | -| ------------------- | ------------ | -| `tap_to_click` | `com.apple.AppleMultitouchTrackpad.Clicking`, `com.apple.driver.AppleBluetoothMultitouch.trackpad.Clicking` | +| Key | Raw defaults | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `tap_to_click` | `com.apple.AppleMultitouchTrackpad.Clicking`, `com.apple.driver.AppleBluetoothMultitouch.trackpad.Clicking` | | `three_finger_drag` | `com.apple.AppleMultitouchTrackpad.TrackpadThreeFingerDrag`, `com.apple.driver.AppleBluetoothMultitouch.trackpad.TrackpadThreeFingerDrag` | Unknown friendly keys, invalid enum values, and unsupported value types warn diff --git a/src/system/mod.rs b/src/system/mod.rs index 04fe7b4ede..469e14e958 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -253,11 +253,12 @@ fn packages_from_config_files_with_brew_taps( /// the value a global config declared. Unsupported value shapes warn /// (forward compatibility) and are skipped. pub fn defaults_from_config(config: &Config) -> Vec { - let mut friendly: IndexMap<(String, String), toml::Value> = IndexMap::new(); - let mut raw: IndexMap<(String, String), toml::Value> = IndexMap::new(); + let mut merged: IndexMap<(String, String), toml::Value> = IndexMap::new(); // config_files is ordered local -> global; reverse for global -> local for cf in config.config_files.values().rev() { if let Some(sys) = cf.bootstrap_config() { + let mut friendly: IndexMap<(String, String), toml::Value> = IndexMap::new(); + let mut raw: IndexMap<(String, String), toml::Value> = IndexMap::new(); merge_friendly_macos_defaults(&mut friendly, &sys.macos); for (domain, entries) in sys.macos.defaults { match entries { @@ -271,9 +272,11 @@ pub fn defaults_from_config(config: &Config) -> Vec { ), } } + for (key, value) in merge_raw_over_friendly_macos_defaults(friendly, raw) { + merged.insert(key, value); + } } } - let merged = merge_raw_over_friendly_macos_defaults(friendly, raw); let mut out = vec![]; for ((domain, key), value) in merged { match DefaultsValue::from_toml(&value) { @@ -340,13 +343,39 @@ fn insert_friendly_default( defaults_key: &str, value: toml::Value, expected: fn(&toml::Value) -> bool, + expected_type: &str, ) { if expected(&value) { out.insert((domain.to_string(), defaults_key.to_string()), value); } else { warn!( "[bootstrap.macos.{section}].{key}: unsupported value type \ - (expected bool, integer, float, or string)" + (expected {expected_type})" + ); + } +} + +fn insert_friendly_multi_domain_default( + out: &mut IndexMap<(String, String), toml::Value>, + section: &str, + key: &str, + domains: &[&str], + defaults_key: &str, + value: toml::Value, + expected: fn(&toml::Value) -> bool, + expected_type: &str, +) { + if expected(&value) { + for domain in domains { + out.insert( + (domain.to_string(), defaults_key.to_string()), + value.clone(), + ); + } + } else { + warn!( + "[bootstrap.macos.{section}].{key}: unsupported value type \ + (expected {expected_type})" ); } } @@ -373,6 +402,7 @@ fn merge_dock_defaults( "autohide", value.clone(), is_bool, + "bool", ), "orientation" => match value { toml::Value::String(s) if matches!(s.as_str(), "bottom" | "left" | "right") => { @@ -397,6 +427,7 @@ fn merge_dock_defaults( "tilesize", value.clone(), is_integer, + "integer", ), "magnification" => insert_friendly_default( out, @@ -406,6 +437,7 @@ fn merge_dock_defaults( "magnification", value.clone(), is_bool, + "bool", ), "largesize" => insert_friendly_default( out, @@ -415,6 +447,7 @@ fn merge_dock_defaults( "largesize", value.clone(), is_integer, + "integer", ), "show_recents" => insert_friendly_default( out, @@ -424,6 +457,7 @@ fn merge_dock_defaults( "show-recents", value.clone(), is_bool, + "bool", ), "mru_spaces" => insert_friendly_default( out, @@ -433,6 +467,7 @@ fn merge_dock_defaults( "mru-spaces", value.clone(), is_bool, + "bool", ), _ => warn!("[bootstrap.macos.dock].{key}: unknown key, ignoring entry"), } @@ -453,6 +488,7 @@ fn merge_finder_defaults( "AppleShowAllFiles", value.clone(), is_bool, + "bool", ), "show_pathbar" => insert_friendly_default( out, @@ -462,6 +498,7 @@ fn merge_finder_defaults( "ShowPathbar", value.clone(), is_bool, + "bool", ), "show_status_bar" => insert_friendly_default( out, @@ -471,6 +508,7 @@ fn merge_finder_defaults( "ShowStatusBar", value.clone(), is_bool, + "bool", ), "show_extensions_warning" => insert_friendly_default( out, @@ -480,6 +518,7 @@ fn merge_finder_defaults( "FXEnableExtensionChangeWarning", value.clone(), is_bool, + "bool", ), "preferred_view_style" => match value { toml::Value::String(s) => { @@ -529,6 +568,7 @@ fn merge_keyboard_defaults( "KeyRepeat", value.clone(), is_integer, + "integer", ), "initial_key_repeat" => insert_friendly_default( out, @@ -538,6 +578,7 @@ fn merge_keyboard_defaults( "InitialKeyRepeat", value.clone(), is_integer, + "integer", ), "press_and_hold" => insert_friendly_default( out, @@ -547,6 +588,7 @@ fn merge_keyboard_defaults( "ApplePressAndHoldEnabled", value.clone(), is_bool, + "bool", ), "fn_state" => insert_friendly_default( out, @@ -556,6 +598,7 @@ fn merge_keyboard_defaults( "com.apple.keyboard.fnState", value.clone(), is_bool, + "bool", ), _ => warn!("[bootstrap.macos.keyboard].{key}: unknown key, ignoring entry"), } @@ -568,38 +611,32 @@ fn merge_trackpad_defaults( ) { for (key, value) in entries { match key.as_str() { - "tap_to_click" => { - for domain in [ + "tap_to_click" => insert_friendly_multi_domain_default( + out, + "trackpad", + key, + &[ "com.apple.AppleMultitouchTrackpad", "com.apple.driver.AppleBluetoothMultitouch.trackpad", - ] { - insert_friendly_default( - out, - "trackpad", - key, - domain, - "Clicking", - value.clone(), - is_bool, - ); - } - } - "three_finger_drag" => { - for domain in [ + ], + "Clicking", + value.clone(), + is_bool, + "bool", + ), + "three_finger_drag" => insert_friendly_multi_domain_default( + out, + "trackpad", + key, + &[ "com.apple.AppleMultitouchTrackpad", "com.apple.driver.AppleBluetoothMultitouch.trackpad", - ] { - insert_friendly_default( - out, - "trackpad", - key, - domain, - "TrackpadThreeFingerDrag", - value.clone(), - is_bool, - ); - } - } + ], + "TrackpadThreeFingerDrag", + value.clone(), + is_bool, + "bool", + ), _ => warn!("[bootstrap.macos.trackpad].{key}: unknown key, ignoring entry"), } } @@ -989,4 +1026,34 @@ mod tests { Some(&toml::Value::Integer(48)) ); } + + #[test] + fn test_local_friendly_macos_defaults_override_global_raw_defaults() { + let mut merged = IndexMap::new(); + + let global_friendly = IndexMap::new(); + let mut global_raw = IndexMap::new(); + global_raw.insert( + ("com.apple.dock".into(), "autohide".into()), + toml::Value::Boolean(false), + ); + for (key, value) in merge_raw_over_friendly_macos_defaults(global_friendly, global_raw) { + merged.insert(key, value); + } + + let mut local_friendly = IndexMap::new(); + local_friendly.insert( + ("com.apple.dock".into(), "autohide".into()), + toml::Value::Boolean(true), + ); + let local_raw = IndexMap::new(); + for (key, value) in merge_raw_over_friendly_macos_defaults(local_friendly, local_raw) { + merged.insert(key, value); + } + + assert_eq!( + merged.get(&("com.apple.dock".into(), "autohide".into())), + Some(&toml::Value::Boolean(true)) + ); + } } From 35d65e460bbc60dd57f950c427efe0d19ef2e352 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:16:21 +0000 Subject: [PATCH 3/5] fix(bootstrap): satisfy macos defaults lint --- docs/bootstrap/macos-defaults.md | 6 +- src/system/mod.rs | 233 ++++++++++++++++++------------- 2 files changed, 143 insertions(+), 96 deletions(-) diff --git a/docs/bootstrap/macos-defaults.md b/docs/bootstrap/macos-defaults.md index f383c0b462..0580b73c23 100644 --- a/docs/bootstrap/macos-defaults.md +++ b/docs/bootstrap/macos-defaults.md @@ -30,8 +30,10 @@ tap_to_click = true The curated sections compile to raw defaults entries. Use `[bootstrap.macos.defaults]` for preferences not covered by the friendly -sections, or to override the raw `(domain, key)` generated by a friendly -setting. +sections. Within the same config file, raw defaults override the raw +`(domain, key)` generated by a friendly setting. Across config files, normal +global to local precedence still applies, so a local friendly setting can +override a global raw default for the same pair. ## Friendly sections diff --git a/src/system/mod.rs b/src/system/mod.rs index 469e14e958..1064feec64 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -335,19 +335,30 @@ fn merge_friendly_macos_defaults( merge_trackpad_defaults(out, &macos.trackpad); } +#[derive(Clone, Copy)] +struct FriendlyDefaultSpec<'a> { + section: &'a str, + key: &'a str, + defaults_key: &'a str, + expected: fn(&toml::Value) -> bool, + expected_type: &'a str, +} + fn insert_friendly_default( out: &mut IndexMap<(String, String), toml::Value>, - section: &str, - key: &str, domain: &str, - defaults_key: &str, + spec: FriendlyDefaultSpec<'_>, value: toml::Value, - expected: fn(&toml::Value) -> bool, - expected_type: &str, ) { - if expected(&value) { - out.insert((domain.to_string(), defaults_key.to_string()), value); + if (spec.expected)(&value) { + out.insert((domain.to_string(), spec.defaults_key.to_string()), value); } else { + let FriendlyDefaultSpec { + section, + key, + expected_type, + .. + } = spec; warn!( "[bootstrap.macos.{section}].{key}: unsupported value type \ (expected {expected_type})" @@ -357,22 +368,24 @@ fn insert_friendly_default( fn insert_friendly_multi_domain_default( out: &mut IndexMap<(String, String), toml::Value>, - section: &str, - key: &str, domains: &[&str], - defaults_key: &str, + spec: FriendlyDefaultSpec<'_>, value: toml::Value, - expected: fn(&toml::Value) -> bool, - expected_type: &str, ) { - if expected(&value) { + if (spec.expected)(&value) { for domain in domains { out.insert( - (domain.to_string(), defaults_key.to_string()), + (domain.to_string(), spec.defaults_key.to_string()), value.clone(), ); } } else { + let FriendlyDefaultSpec { + section, + key, + expected_type, + .. + } = spec; warn!( "[bootstrap.macos.{section}].{key}: unsupported value type \ (expected {expected_type})" @@ -396,13 +409,15 @@ fn merge_dock_defaults( match key.as_str() { "autohide" => insert_friendly_default( out, - "dock", - key, "com.apple.dock", - "autohide", + FriendlyDefaultSpec { + section: "dock", + key, + defaults_key: "autohide", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), "orientation" => match value { toml::Value::String(s) if matches!(s.as_str(), "bottom" | "left" | "right") => { @@ -421,53 +436,63 @@ fn merge_dock_defaults( }, "tilesize" => insert_friendly_default( out, - "dock", - key, "com.apple.dock", - "tilesize", + FriendlyDefaultSpec { + section: "dock", + key, + defaults_key: "tilesize", + expected: is_integer, + expected_type: "integer", + }, value.clone(), - is_integer, - "integer", ), "magnification" => insert_friendly_default( out, - "dock", - key, "com.apple.dock", - "magnification", + FriendlyDefaultSpec { + section: "dock", + key, + defaults_key: "magnification", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), "largesize" => insert_friendly_default( out, - "dock", - key, "com.apple.dock", - "largesize", + FriendlyDefaultSpec { + section: "dock", + key, + defaults_key: "largesize", + expected: is_integer, + expected_type: "integer", + }, value.clone(), - is_integer, - "integer", ), "show_recents" => insert_friendly_default( out, - "dock", - key, "com.apple.dock", - "show-recents", + FriendlyDefaultSpec { + section: "dock", + key, + defaults_key: "show-recents", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), "mru_spaces" => insert_friendly_default( out, - "dock", - key, "com.apple.dock", - "mru-spaces", + FriendlyDefaultSpec { + section: "dock", + key, + defaults_key: "mru-spaces", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), _ => warn!("[bootstrap.macos.dock].{key}: unknown key, ignoring entry"), } @@ -482,43 +507,51 @@ fn merge_finder_defaults( match key.as_str() { "show_all_files" => insert_friendly_default( out, - "finder", - key, "com.apple.finder", - "AppleShowAllFiles", + FriendlyDefaultSpec { + section: "finder", + key, + defaults_key: "AppleShowAllFiles", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), "show_pathbar" => insert_friendly_default( out, - "finder", - key, "com.apple.finder", - "ShowPathbar", + FriendlyDefaultSpec { + section: "finder", + key, + defaults_key: "ShowPathbar", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), "show_status_bar" => insert_friendly_default( out, - "finder", - key, "com.apple.finder", - "ShowStatusBar", + FriendlyDefaultSpec { + section: "finder", + key, + defaults_key: "ShowStatusBar", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), "show_extensions_warning" => insert_friendly_default( out, - "finder", - key, "com.apple.finder", - "FXEnableExtensionChangeWarning", + FriendlyDefaultSpec { + section: "finder", + key, + defaults_key: "FXEnableExtensionChangeWarning", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), "preferred_view_style" => match value { toml::Value::String(s) => { @@ -562,43 +595,51 @@ fn merge_keyboard_defaults( match key.as_str() { "key_repeat" => insert_friendly_default( out, - "keyboard", - key, "NSGlobalDomain", - "KeyRepeat", + FriendlyDefaultSpec { + section: "keyboard", + key, + defaults_key: "KeyRepeat", + expected: is_integer, + expected_type: "integer", + }, value.clone(), - is_integer, - "integer", ), "initial_key_repeat" => insert_friendly_default( out, - "keyboard", - key, "NSGlobalDomain", - "InitialKeyRepeat", + FriendlyDefaultSpec { + section: "keyboard", + key, + defaults_key: "InitialKeyRepeat", + expected: is_integer, + expected_type: "integer", + }, value.clone(), - is_integer, - "integer", ), "press_and_hold" => insert_friendly_default( out, - "keyboard", - key, "NSGlobalDomain", - "ApplePressAndHoldEnabled", + FriendlyDefaultSpec { + section: "keyboard", + key, + defaults_key: "ApplePressAndHoldEnabled", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), "fn_state" => insert_friendly_default( out, - "keyboard", - key, "NSGlobalDomain", - "com.apple.keyboard.fnState", + FriendlyDefaultSpec { + section: "keyboard", + key, + defaults_key: "com.apple.keyboard.fnState", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), _ => warn!("[bootstrap.macos.keyboard].{key}: unknown key, ignoring entry"), } @@ -613,29 +654,33 @@ fn merge_trackpad_defaults( match key.as_str() { "tap_to_click" => insert_friendly_multi_domain_default( out, - "trackpad", - key, &[ "com.apple.AppleMultitouchTrackpad", "com.apple.driver.AppleBluetoothMultitouch.trackpad", ], - "Clicking", + FriendlyDefaultSpec { + section: "trackpad", + key, + defaults_key: "Clicking", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), "three_finger_drag" => insert_friendly_multi_domain_default( out, - "trackpad", - key, &[ "com.apple.AppleMultitouchTrackpad", "com.apple.driver.AppleBluetoothMultitouch.trackpad", ], - "TrackpadThreeFingerDrag", + FriendlyDefaultSpec { + section: "trackpad", + key, + defaults_key: "TrackpadThreeFingerDrag", + expected: is_bool, + expected_type: "bool", + }, value.clone(), - is_bool, - "bool", ), _ => warn!("[bootstrap.macos.trackpad].{key}: unknown key, ignoring entry"), } From 8ca17c762143158f2e788d746baafdae1c50e2df Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:27:14 +0000 Subject: [PATCH 4/5] fix(schema): add friendly macos bootstrap defaults --- schema/mise.json | 102 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/schema/mise.json b/schema/mise.json index 20887c91a1..e1ef11af57 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -3050,6 +3050,108 @@ "type": "object", "description": "macOS-specific bootstrap config", "properties": { + "dock": { + "type": "object", + "description": "curated Dock preferences that compile into macOS defaults", + "properties": { + "autohide": { + "type": "boolean", + "description": "hide and show the Dock automatically" + }, + "orientation": { + "type": "string", + "enum": ["bottom", "left", "right"], + "description": "Dock screen edge" + }, + "tilesize": { + "type": "integer", + "description": "Dock icon size" + }, + "magnification": { + "type": "boolean", + "description": "enable Dock magnification" + }, + "largesize": { + "type": "integer", + "description": "Dock magnified icon size" + }, + "show_recents": { + "type": "boolean", + "description": "show recent applications in the Dock" + }, + "mru_spaces": { + "type": "boolean", + "description": "automatically rearrange Spaces based on most recent use" + } + }, + "additionalProperties": false + }, + "finder": { + "type": "object", + "description": "curated Finder preferences that compile into macOS defaults", + "properties": { + "show_all_files": { + "type": "boolean", + "description": "show hidden files in Finder" + }, + "show_pathbar": { + "type": "boolean", + "description": "show the Finder path bar" + }, + "show_status_bar": { + "type": "boolean", + "description": "show the Finder status bar" + }, + "show_extensions_warning": { + "type": "boolean", + "description": "show the warning when changing file extensions" + }, + "preferred_view_style": { + "type": "string", + "enum": ["icon", "list", "column", "gallery"], + "description": "Finder preferred view style" + } + }, + "additionalProperties": false + }, + "keyboard": { + "type": "object", + "description": "curated keyboard preferences that compile into macOS defaults", + "properties": { + "key_repeat": { + "type": "integer", + "description": "keyboard repeat interval" + }, + "initial_key_repeat": { + "type": "integer", + "description": "delay before key repeat starts" + }, + "press_and_hold": { + "type": "boolean", + "description": "enable press-and-hold accent picker" + }, + "fn_state": { + "type": "boolean", + "description": "use F1, F2, etc. as standard function keys" + } + }, + "additionalProperties": false + }, + "trackpad": { + "type": "object", + "description": "curated trackpad preferences that compile into macOS defaults", + "properties": { + "tap_to_click": { + "type": "boolean", + "description": "enable tap to click for built-in and Bluetooth trackpads" + }, + "three_finger_drag": { + "type": "boolean", + "description": "enable three finger drag for built-in and Bluetooth trackpads" + } + }, + "additionalProperties": false + }, "defaults": { "type": "object", "description": "macOS user defaults to apply with `mise bootstrap macos-defaults apply`, keyed by preferences domain (e.g. \"com.apple.dock\", \"NSGlobalDomain\")", From fac98df760d9e3f16969533e747a6ea3c16b1446 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:35:31 +0000 Subject: [PATCH 5/5] fix(oci): reject friendly macos defaults --- src/cli/oci/common.rs | 9 ++------- src/system/mod.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/cli/oci/common.rs b/src/cli/oci/common.rs index 893c839cbd..302bef9d12 100644 --- a/src/cli/oci/common.rs +++ b/src/cli/oci/common.rs @@ -123,17 +123,12 @@ fn reject_unsupported_system_defaults(config_files: &ConfigMap) -> Result<()> { let Some(system) = cf.bootstrap_config() else { continue; }; - defaults += system - .macos - .defaults - .values() - .map(|v| v.as_table().map_or(1, |t| t.len())) - .sum::(); + defaults += system::macos_defaults_entry_count(&system.macos); } if defaults > 0 { bail!( - "mise oci does not support [bootstrap.macos.defaults] (found {defaults} default entries); \ + "mise oci does not support [bootstrap.macos.*] defaults (found {defaults} default entries); \ macOS defaults do not apply to OCI images." ); } diff --git a/src/system/mod.rs b/src/system/mod.rs index 1064feec64..b351f011b1 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -315,6 +315,26 @@ pub fn launchd_from_config(config: &Config) -> Vec { out } +/// Count macOS defaults declared in one config file, including friendly +/// sections that compile into raw defaults entries. +pub fn macos_defaults_entry_count(macos: &BootstrapMacosTomlConfig) -> usize { + let mut friendly: IndexMap<(String, String), toml::Value> = IndexMap::new(); + let mut raw: IndexMap<(String, String), toml::Value> = IndexMap::new(); + let mut malformed_domains = 0usize; + merge_friendly_macos_defaults(&mut friendly, macos); + for (domain, entries) in &macos.defaults { + match entries { + toml::Value::Table(entries) => { + for (key, value) in entries { + raw.insert((domain.clone(), key.clone()), value.clone()); + } + } + _ => malformed_domains += 1, + } + } + merge_raw_over_friendly_macos_defaults(friendly, raw).len() + malformed_domains +} + fn merge_raw_over_friendly_macos_defaults( mut friendly: IndexMap<(String, String), toml::Value>, raw: IndexMap<(String, String), toml::Value>, @@ -1101,4 +1121,18 @@ mod tests { Some(&toml::Value::Boolean(true)) ); } + + #[test] + fn test_macos_defaults_entry_count_includes_friendly_defaults() { + let mut macos = BootstrapMacosTomlConfig::default(); + macos.dock.insert("autohide".into(), tv("true")); + macos.trackpad.insert("tap_to_click".into(), tv("true")); + macos.defaults.insert( + "com.apple.dock".into(), + tv(r#"{ autohide = false, tilesize = 48 }"#), + ); + macos.defaults.insert("malformed".into(), tv("true")); + + assert_eq!(macos_defaults_entry_count(&macos), 5); + } }