diff --git a/docs/resources/(resources)/macos-settings.mdx b/docs/resources/(resources)/macos-settings.mdx new file mode 100644 index 0000000..0d3dc0c --- /dev/null +++ b/docs/resources/(resources)/macos-settings.mdx @@ -0,0 +1,135 @@ +--- +title: macos-settings +description: A reference page for the macos-settings resource +--- + +The macos-settings resource manages common macOS system preferences using the built-in `defaults` command. It covers mouse, keyboard, trackpad, and Dock settings — everything you need to reproduce your preferred system configuration on a new Mac. + +## Parameters + +All sections and their sub-keys are optional. You only need to declare the settings you want to manage. + +### `mouse` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `naturalScrolling` | boolean | `true` | Scroll content in the natural direction (content follows finger). When `false`, uses the traditional scroll direction. | +| `acceleration` | boolean | `true` | Enable mouse acceleration. When `false`, the cursor moves at a fixed speed regardless of how fast the mouse is moved. | +| `speed` | number (0–3) | `1.5` | Mouse tracking speed. Higher values make the cursor move farther per physical movement. | + +### `keyboard` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `keyRepeat` | integer | `6` | Rate of key repeat while a key is held. Lower = faster (1 is fastest; 120 effectively disables repeat). | +| `initialKeyRepeat` | integer | `68` | Delay before key repeat begins (in ticks). Lower = shorter delay (10 minimum). | +| `pressAndHold` | boolean | `true` | When `true`, holding a key shows the accent character picker. When `false`, the key repeats instead. | +| `fnKeysAsStandardKeys` | boolean | `false` | When `true`, the F1–F12 keys act as standard function keys; press Fn to trigger special actions (brightness, volume, etc.). | +| `keyboardNavigation` | boolean | `false` | When `true`, enables Tab-based focus navigation in system dialogs (equivalent to "Keyboard navigation" in System Settings). | + +### `trackpad` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `speed` | number (0–3) | `1.5` | Trackpad tracking speed. Higher values make the cursor move farther per swipe distance. | + +### `dock` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `position` | `"left"` \| `"bottom"` \| `"right"` | `"bottom"` | Position of the Dock on screen. | +| `iconSize` | integer (16–128) | `48` | Dock icon size in pixels. | +| `autohide` | boolean | `false` | Automatically hide and show the Dock when the cursor moves near the screen edge. | +| `autohideDelay` | number | `0.2` | Seconds to wait before showing the Dock when it is hidden. Set to `0` for instant reveal. | +| `showRecents` | boolean | `true` | Show recently opened apps in a dedicated section of the Dock. | +| `minimizeEffect` | `"genie"` \| `"scale"` \| `"suck"` | `"genie"` | Window minimize animation style. | + +## macOS defaults mapping + +The table below shows the underlying `defaults` key used for each friendly parameter name. + +| Section | Parameter | Domain | Key | +|---------|-----------|--------|-----| +| mouse | `naturalScrolling` | `NSGlobalDomain` | `com.apple.swipescrolldirection` | +| mouse | `acceleration` | `NSGlobalDomain` | `com.apple.mouse.linear` (inverted) | +| mouse | `speed` | `NSGlobalDomain` | `com.apple.mouse.scaling` | +| keyboard | `keyRepeat` | `NSGlobalDomain` | `KeyRepeat` | +| keyboard | `initialKeyRepeat` | `NSGlobalDomain` | `InitialKeyRepeat` | +| keyboard | `pressAndHold` | `NSGlobalDomain` | `ApplePressAndHoldEnabled` | +| keyboard | `fnKeysAsStandardKeys` | `NSGlobalDomain` | `com.apple.keyboard.fnState` | +| keyboard | `keyboardNavigation` | `NSGlobalDomain` | `AppleKeyboardUIMode` (0/2) | +| trackpad | `speed` | `NSGlobalDomain` | `com.apple.trackpad.scaling` | +| dock | `position` | `com.apple.dock` | `orientation` | +| dock | `iconSize` | `com.apple.dock` | `tilesize` | +| dock | `autohide` | `com.apple.dock` | `autohide` | +| dock | `autohideDelay` | `com.apple.dock` | `autohide-delay` | +| dock | `showRecents` | `com.apple.dock` | `show-recents` | +| dock | `minimizeEffect` | `com.apple.dock` | `mineffect` | + +## Example usage + +### Common macOS preferences + +```json title="codify.jsonc" +[ + { + "type": "macos-settings", + "os": ["macOS"], + "mouse": { + "naturalScrolling": true + }, + "keyboard": { + "keyRepeat": 2, + "initialKeyRepeat": 15, + "pressAndHold": false + }, + "dock": { + "position": "left", + "iconSize": 36, + "autohide": true, + "showRecents": false + } + } +] +``` + +### Non-Apple keyboard setup + +```json title="codify.jsonc" +[ + { + "type": "macos-settings", + "os": ["macOS"], + "mouse": { + "naturalScrolling": false, + "acceleration": false + }, + "keyboard": { + "fnKeysAsStandardKeys": true + } + } +] +``` + +### Trackpad speed only + +```json title="codify.jsonc" +[ + { + "type": "macos-settings", + "os": ["macOS"], + "trackpad": { + "speed": 2.5 + } + } +] +``` + +## Notes + +- This resource is **macOS only** and has no effect on Linux. +- No software installation is required — `defaults` is a built-in macOS command. +- Dock settings take effect immediately (the Dock is automatically restarted). Other settings typically take effect the next time you open an application or after logging out. +- When the resource is removed from your configuration, all managed settings are reset to their macOS system defaults using `defaults delete`. +- Changes to `fnKeysAsStandardKeys` may require a full system restart to take effect. +- The `keyRepeat` and `initialKeyRepeat` values use macOS internal tick units, not milliseconds. Smaller values produce faster key repeat. diff --git a/src/index.ts b/src/index.ts index 5f68f6a..4a0ec45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import { Npm } from './resources/javascript/npm/npm.js'; import { NpmLoginResource } from './resources/javascript/npm/npm-login.js'; import { NvmResource } from './resources/javascript/nvm/nvm.js'; import { Pnpm } from './resources/javascript/pnpm/pnpm.js'; +import { MacosSettingsResource } from './resources/macos/macos-settings/macos-settings-resource.js'; import { MacportsResource } from './resources/macports/macports.js'; import { OllamaResource } from './resources/ollama/ollama.js'; import { PgcliResource } from './resources/pgcli/pgcli.js'; @@ -97,6 +98,7 @@ runPlugin(Plugin.create( new Pip(), new PipSync(), new MacportsResource(), + new MacosSettingsResource(), new Npm(), new NpmLoginResource(), new DockerResource(), diff --git a/src/resources/macos/macos-settings/macos-settings-resource.ts b/src/resources/macos/macos-settings/macos-settings-resource.ts new file mode 100644 index 0000000..e158118 --- /dev/null +++ b/src/resources/macos/macos-settings/macos-settings-resource.ts @@ -0,0 +1,476 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; + +const mouseSchema = z.object({ + naturalScrolling: z.boolean().optional(), + acceleration: z.boolean().optional(), + speed: z.number().min(0).max(3).optional(), +}).optional(); + +const keyboardSchema = z.object({ + keyRepeat: z.number().int().min(1).optional(), + initialKeyRepeat: z.number().int().min(10).optional(), + pressAndHold: z.boolean().optional(), + fnKeysAsStandardKeys: z.boolean().optional(), + keyboardNavigation: z.boolean().optional(), +}).optional(); + +const trackpadSchema = z.object({ + speed: z.number().min(0).max(3).optional(), +}).optional(); + +const dockSchema = z.object({ + position: z.enum(['left', 'bottom', 'right']).optional(), + iconSize: z.number().int().min(16).max(128).optional(), + autohide: z.boolean().optional(), + autohideDelay: z.number().min(0).optional(), + showRecents: z.boolean().optional(), + minimizeEffect: z.enum(['genie', 'scale', 'suck']).optional(), +}).optional(); + +export const schema = z.object({ + mouse: mouseSchema, + keyboard: keyboardSchema, + trackpad: trackpadSchema, + dock: dockSchema, +}); + +export type MacosSettingsConfig = z.infer; +type MouseConfig = NonNullable; +type KeyboardConfig = NonNullable; +type TrackpadConfig = NonNullable; +type DockConfig = NonNullable; + +const defaultConfig: Partial = { + mouse: { + naturalScrolling: true, + speed: 1.5, + }, + keyboard: { + keyRepeat: 6, + initialKeyRepeat: 68, + pressAndHold: true, + fnKeysAsStandardKeys: false, + }, + dock: { + position: 'bottom', + iconSize: 48, + autohide: false, + showRecents: true, + minimizeEffect: 'genie', + }, +}; + +const exampleCommonPrefs: ExampleConfig = { + title: 'Common macOS preferences', + description: 'Configure natural scrolling, fast key repeat, and a minimal Dock for a consistent setup on any new Mac.', + configs: [{ + type: 'macos-settings', + os: ['macOS'], + mouse: { + naturalScrolling: true, + }, + keyboard: { + keyRepeat: 2, + initialKeyRepeat: 15, + pressAndHold: false, + }, + dock: { + position: 'left', + iconSize: 36, + autohide: true, + showRecents: false, + }, + }], +}; + +const exampleNonAppleKeyboard: ExampleConfig = { + title: 'Non-Apple keyboard setup', + description: 'Disable natural scrolling and enable standard function keys for a non-Apple keyboard or mouse.', + configs: [{ + type: 'macos-settings', + os: ['macOS'], + mouse: { + naturalScrolling: false, + acceleration: false, + }, + keyboard: { + fnKeysAsStandardKeys: true, + }, + }], +}; + +export class MacosSettingsResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'macos-settings', + operatingSystems: [OS.Darwin], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleCommonPrefs, + example2: exampleNonAppleKeyboard, + }, + parameterSettings: { + mouse: { type: 'object', canModify: true }, + keyboard: { type: 'object', canModify: true }, + trackpad: { type: 'object', canModify: true }, + dock: { type: 'object', canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const result: Partial = {}; + let anyFound = false; + + if (parameters.mouse) { + const mouse = await this.readMouseSettings(parameters.mouse); + if (mouse !== null) { result.mouse = mouse; anyFound = true; } + } + if (parameters.keyboard) { + const keyboard = await this.readKeyboardSettings(parameters.keyboard); + if (keyboard !== null) { result.keyboard = keyboard; anyFound = true; } + } + if (parameters.trackpad) { + const trackpad = await this.readTrackpadSettings(parameters.trackpad); + if (trackpad !== null) { result.trackpad = trackpad; anyFound = true; } + } + if (parameters.dock) { + const dock = await this.readDockSettings(parameters.dock); + if (dock !== null) { result.dock = dock; anyFound = true; } + } + + return anyFound ? result : null; + } + + override async create(plan: CreatePlan): Promise { + const { desiredConfig } = plan; + + if (desiredConfig.mouse) { + await this.applyMouseSettings(desiredConfig.mouse); + } + if (desiredConfig.keyboard) { + await this.applyKeyboardSettings(desiredConfig.keyboard); + } + if (desiredConfig.trackpad) { + await this.applyTrackpadSettings(desiredConfig.trackpad); + } + if (desiredConfig.dock) { + await this.applyDockSettings(desiredConfig.dock); + } + } + + override async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + const { desiredConfig } = plan; + + if (pc.name === 'mouse' && desiredConfig.mouse) { + await this.applyMouseSettings(desiredConfig.mouse); + } else if (pc.name === 'keyboard' && desiredConfig.keyboard) { + await this.applyKeyboardSettings(desiredConfig.keyboard); + } else if (pc.name === 'trackpad' && desiredConfig.trackpad) { + await this.applyTrackpadSettings(desiredConfig.trackpad); + } else if (pc.name === 'dock' && desiredConfig.dock) { + await this.applyDockSettings(desiredConfig.dock); + } + } + + override async destroy(plan: DestroyPlan): Promise { + const { currentConfig } = plan; + const $ = getPty(); + let dockChanged = false; + + if (currentConfig.mouse) { + await this.deleteMouseSettings(currentConfig.mouse); + } + if (currentConfig.keyboard) { + await this.deleteKeyboardSettings(currentConfig.keyboard); + } + if (currentConfig.trackpad) { + await this.deleteTrackpadSettings(currentConfig.trackpad); + } + if (currentConfig.dock) { + await this.deleteDockSettings(currentConfig.dock); + dockChanged = true; + } + + if (dockChanged) { + await $.spawnSafe('killall Dock'); + } + } + + // ---- Mouse ---- + + private async readMouseSettings(desired: MouseConfig): Promise { + const result: MouseConfig = {}; + let anyFound = false; + + if ('naturalScrolling' in desired) { + const v = await this.readBool('NSGlobalDomain', 'com.apple.swipescrolldirection'); + if (v !== null) { result.naturalScrolling = v; anyFound = true; } + } + if ('acceleration' in desired) { + const linear = await this.readBool('NSGlobalDomain', 'com.apple.mouse.linear'); + // com.apple.mouse.linear=true means acceleration is DISABLED; invert for user-friendly name + if (linear !== null) { result.acceleration = !linear; anyFound = true; } + } + if ('speed' in desired) { + const v = await this.readFloat('NSGlobalDomain', 'com.apple.mouse.scaling'); + if (v !== null) { result.speed = v; anyFound = true; } + } + + return anyFound ? result : null; + } + + private async applyMouseSettings(settings: MouseConfig): Promise { + const $ = getPty(); + + if (settings.naturalScrolling !== undefined) { + await $.spawn(`defaults write NSGlobalDomain com.apple.swipescrolldirection -bool ${settings.naturalScrolling}`); + } + if (settings.acceleration !== undefined) { + // linear=true means no acceleration; invert the user-facing boolean + await $.spawn(`defaults write NSGlobalDomain com.apple.mouse.linear -bool ${!settings.acceleration}`); + } + if (settings.speed !== undefined) { + await $.spawn(`defaults write NSGlobalDomain com.apple.mouse.scaling -float ${settings.speed}`); + } + } + + private async deleteMouseSettings(settings: MouseConfig): Promise { + const $ = getPty(); + + if ('naturalScrolling' in settings) { + await $.spawnSafe('defaults delete NSGlobalDomain com.apple.swipescrolldirection'); + } + if ('acceleration' in settings) { + await $.spawnSafe('defaults delete NSGlobalDomain com.apple.mouse.linear'); + } + if ('speed' in settings) { + await $.spawnSafe('defaults delete NSGlobalDomain com.apple.mouse.scaling'); + } + } + + // ---- Keyboard ---- + + private async readKeyboardSettings(desired: KeyboardConfig): Promise { + const result: KeyboardConfig = {}; + let anyFound = false; + + if ('keyRepeat' in desired) { + const v = await this.readInt('NSGlobalDomain', 'KeyRepeat'); + if (v !== null) { result.keyRepeat = v; anyFound = true; } + } + if ('initialKeyRepeat' in desired) { + const v = await this.readInt('NSGlobalDomain', 'InitialKeyRepeat'); + if (v !== null) { result.initialKeyRepeat = v; anyFound = true; } + } + if ('pressAndHold' in desired) { + const v = await this.readBool('NSGlobalDomain', 'ApplePressAndHoldEnabled'); + if (v !== null) { result.pressAndHold = v; anyFound = true; } + } + if ('fnKeysAsStandardKeys' in desired) { + const v = await this.readBool('NSGlobalDomain', 'com.apple.keyboard.fnState'); + if (v !== null) { result.fnKeysAsStandardKeys = v; anyFound = true; } + } + if ('keyboardNavigation' in desired) { + const v = await this.readInt('NSGlobalDomain', 'AppleKeyboardUIMode'); + // AppleKeyboardUIMode: 0=disabled, 2=enabled + if (v !== null) { result.keyboardNavigation = v === 2; anyFound = true; } + } + + return anyFound ? result : null; + } + + private async applyKeyboardSettings(settings: KeyboardConfig): Promise { + const $ = getPty(); + + if (settings.keyRepeat !== undefined) { + await $.spawn(`defaults write NSGlobalDomain KeyRepeat -int ${settings.keyRepeat}`); + } + if (settings.initialKeyRepeat !== undefined) { + await $.spawn(`defaults write NSGlobalDomain InitialKeyRepeat -int ${settings.initialKeyRepeat}`); + } + if (settings.pressAndHold !== undefined) { + await $.spawn(`defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool ${settings.pressAndHold}`); + } + if (settings.fnKeysAsStandardKeys !== undefined) { + await $.spawn(`defaults write NSGlobalDomain com.apple.keyboard.fnState -bool ${settings.fnKeysAsStandardKeys}`); + } + if (settings.keyboardNavigation !== undefined) { + // Map boolean to the int value macOS expects (0=disabled, 2=enabled) + await $.spawn(`defaults write NSGlobalDomain AppleKeyboardUIMode -int ${settings.keyboardNavigation ? 2 : 0}`); + } + } + + private async deleteKeyboardSettings(settings: KeyboardConfig): Promise { + const $ = getPty(); + + if ('keyRepeat' in settings) { + await $.spawnSafe('defaults delete NSGlobalDomain KeyRepeat'); + } + if ('initialKeyRepeat' in settings) { + await $.spawnSafe('defaults delete NSGlobalDomain InitialKeyRepeat'); + } + if ('pressAndHold' in settings) { + await $.spawnSafe('defaults delete NSGlobalDomain ApplePressAndHoldEnabled'); + } + if ('fnKeysAsStandardKeys' in settings) { + await $.spawnSafe('defaults delete NSGlobalDomain com.apple.keyboard.fnState'); + } + if ('keyboardNavigation' in settings) { + await $.spawnSafe('defaults delete NSGlobalDomain AppleKeyboardUIMode'); + } + } + + // ---- Trackpad ---- + + private async readTrackpadSettings(desired: TrackpadConfig): Promise { + const result: TrackpadConfig = {}; + let anyFound = false; + + if ('speed' in desired) { + const v = await this.readFloat('NSGlobalDomain', 'com.apple.trackpad.scaling'); + if (v !== null) { result.speed = v; anyFound = true; } + } + + return anyFound ? result : null; + } + + private async applyTrackpadSettings(settings: TrackpadConfig): Promise { + const $ = getPty(); + + if (settings.speed !== undefined) { + await $.spawn(`defaults write NSGlobalDomain com.apple.trackpad.scaling -float ${settings.speed}`); + } + } + + private async deleteTrackpadSettings(settings: TrackpadConfig): Promise { + const $ = getPty(); + + if ('speed' in settings) { + await $.spawnSafe('defaults delete NSGlobalDomain com.apple.trackpad.scaling'); + } + } + + // ---- Dock ---- + + private async readDockSettings(desired: DockConfig): Promise { + const result: DockConfig = {}; + let anyFound = false; + + if ('position' in desired) { + const v = await this.readString('com.apple.dock', 'orientation'); + if (v !== null) { result.position = v as DockConfig['position']; anyFound = true; } + } + if ('iconSize' in desired) { + const v = await this.readInt('com.apple.dock', 'tilesize'); + if (v !== null) { result.iconSize = v; anyFound = true; } + } + if ('autohide' in desired) { + const v = await this.readBool('com.apple.dock', 'autohide'); + if (v !== null) { result.autohide = v; anyFound = true; } + } + if ('autohideDelay' in desired) { + const v = await this.readFloat('com.apple.dock', 'autohide-delay'); + if (v !== null) { result.autohideDelay = v; anyFound = true; } + } + if ('showRecents' in desired) { + const v = await this.readBool('com.apple.dock', 'show-recents'); + if (v !== null) { result.showRecents = v; anyFound = true; } + } + if ('minimizeEffect' in desired) { + const v = await this.readString('com.apple.dock', 'mineffect'); + if (v !== null) { result.minimizeEffect = v as DockConfig['minimizeEffect']; anyFound = true; } + } + + return anyFound ? result : null; + } + + private async applyDockSettings(settings: DockConfig): Promise { + const $ = getPty(); + + if (settings.position !== undefined) { + await $.spawn(`defaults write com.apple.dock orientation -string "${settings.position}"`); + } + if (settings.iconSize !== undefined) { + await $.spawn(`defaults write com.apple.dock tilesize -int ${settings.iconSize}`); + } + if (settings.autohide !== undefined) { + await $.spawn(`defaults write com.apple.dock autohide -bool ${settings.autohide}`); + } + if (settings.autohideDelay !== undefined) { + await $.spawn(`defaults write com.apple.dock "autohide-delay" -float ${settings.autohideDelay}`); + } + if (settings.showRecents !== undefined) { + await $.spawn(`defaults write com.apple.dock "show-recents" -bool ${settings.showRecents}`); + } + if (settings.minimizeEffect !== undefined) { + await $.spawn(`defaults write com.apple.dock mineffect -string "${settings.minimizeEffect}"`); + } + + await $.spawnSafe('killall Dock'); + } + + private async deleteDockSettings(settings: DockConfig): Promise { + const $ = getPty(); + const keyMap: Record = { + position: 'orientation', + iconSize: 'tilesize', + autohide: 'autohide', + autohideDelay: 'autohide-delay', + showRecents: 'show-recents', + minimizeEffect: 'mineffect', + }; + + for (const [prop, key] of Object.entries(keyMap)) { + if (prop in settings) { + await $.spawnSafe(`defaults delete com.apple.dock "${key}"`); + } + } + } + + // ---- Low-level defaults read helpers ---- + + private async readBool(domain: string, key: string): Promise { + const $ = getPty(); + const { data, status } = await $.spawnSafe(`defaults read "${domain}" "${key}"`); + if (status === SpawnStatus.ERROR) return null; + const val = data.trim(); + return val === '1' || val === 'true' || val === 'YES'; + } + + private async readInt(domain: string, key: string): Promise { + const $ = getPty(); + const { data, status } = await $.spawnSafe(`defaults read "${domain}" "${key}"`); + if (status === SpawnStatus.ERROR) return null; + const val = parseInt(data.trim(), 10); + return isNaN(val) ? null : val; + } + + private async readFloat(domain: string, key: string): Promise { + const $ = getPty(); + const { data, status } = await $.spawnSafe(`defaults read "${domain}" "${key}"`); + if (status === SpawnStatus.ERROR) return null; + const val = parseFloat(data.trim()); + return isNaN(val) ? null : val; + } + + private async readString(domain: string, key: string): Promise { + const $ = getPty(); + const { data, status } = await $.spawnSafe(`defaults read "${domain}" "${key}"`); + if (status === SpawnStatus.ERROR) return null; + return data.trim() || null; + } +} diff --git a/test/macos/macos-settings.test.ts b/test/macos/macos-settings.test.ts new file mode 100644 index 0000000..01be39b --- /dev/null +++ b/test/macos/macos-settings.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from 'vitest'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import * as path from 'node:path'; +import { Utils } from '@codifycli/plugin-core'; + +describe('macos-settings resource integration tests', { skip: !Utils.isMacOS() }, async () => { + const pluginPath = path.resolve('./src/index.ts'); + + it('Can configure mouse natural scrolling', { timeout: 300000 }, async () => { + await PluginTester.fullTest(pluginPath, [ + { + type: 'macos-settings', + mouse: { + naturalScrolling: false, + }, + }, + ], { + validateApply: async () => { + const { data } = await testSpawn('defaults read NSGlobalDomain com.apple.swipescrolldirection'); + expect(data.trim()).toBe('0'); + }, + testModify: { + modifiedConfigs: [{ + type: 'macos-settings', + mouse: { + naturalScrolling: true, + }, + }], + validateModify: async () => { + const { data } = await testSpawn('defaults read NSGlobalDomain com.apple.swipescrolldirection'); + expect(data.trim()).toBe('1'); + }, + }, + validateDestroy: async () => { + // After destroy, the key may be deleted (returns error) or reset to default + const { data } = await testSpawn('defaults read NSGlobalDomain com.apple.swipescrolldirection'); + // Default is true (1) when key is absent, or the key was deleted — either is acceptable + const val = data.trim(); + expect(['', '1'].includes(val) || val.includes('does not exist')).toBe(true); + }, + }); + }); + + it('Can configure Dock settings', { timeout: 300000 }, async () => { + await PluginTester.fullTest(pluginPath, [ + { + type: 'macos-settings', + dock: { + autohide: true, + iconSize: 36, + showRecents: false, + }, + }, + ], { + validateApply: async () => { + const { data: autohide } = await testSpawn('defaults read com.apple.dock autohide'); + expect(autohide.trim()).toBe('1'); + + const { data: tilesize } = await testSpawn('defaults read com.apple.dock tilesize'); + expect(parseInt(tilesize.trim(), 10)).toBe(36); + + const { data: showRecents } = await testSpawn('defaults read com.apple.dock show-recents'); + expect(showRecents.trim()).toBe('0'); + }, + testModify: { + modifiedConfigs: [{ + type: 'macos-settings', + dock: { + autohide: false, + iconSize: 48, + showRecents: true, + }, + }], + validateModify: async () => { + const { data: autohide } = await testSpawn('defaults read com.apple.dock autohide'); + expect(autohide.trim()).toBe('0'); + + const { data: tilesize } = await testSpawn('defaults read com.apple.dock tilesize'); + expect(parseInt(tilesize.trim(), 10)).toBe(48); + }, + }, + validateDestroy: async () => { + // After destroy, keys should be deleted — reads will fail or return defaults + const { data: tilesize } = await testSpawn('defaults read com.apple.dock tilesize'); + const val = tilesize.trim(); + const parsed = parseInt(val, 10); + // Either deleted (error output or NaN) or reset to system default (48) + expect(val.includes('does not exist') || isNaN(parsed) || parsed === 48).toBe(true); + }, + }); + }); + + it('Can configure keyboard settings', { timeout: 300000 }, async () => { + await PluginTester.fullTest(pluginPath, [ + { + type: 'macos-settings', + keyboard: { + keyRepeat: 2, + initialKeyRepeat: 15, + pressAndHold: false, + }, + }, + ], { + validateApply: async () => { + const { data: keyRepeat } = await testSpawn('defaults read NSGlobalDomain KeyRepeat'); + expect(parseInt(keyRepeat.trim(), 10)).toBe(2); + + const { data: initialKeyRepeat } = await testSpawn('defaults read NSGlobalDomain InitialKeyRepeat'); + expect(parseInt(initialKeyRepeat.trim(), 10)).toBe(15); + + const { data: pressAndHold } = await testSpawn('defaults read NSGlobalDomain ApplePressAndHoldEnabled'); + expect(pressAndHold.trim()).toBe('0'); + }, + testModify: { + modifiedConfigs: [{ + type: 'macos-settings', + keyboard: { + keyRepeat: 6, + initialKeyRepeat: 68, + pressAndHold: true, + }, + }], + validateModify: async () => { + const { data: keyRepeat } = await testSpawn('defaults read NSGlobalDomain KeyRepeat'); + expect(parseInt(keyRepeat.trim(), 10)).toBe(6); + + const { data: initialKeyRepeat } = await testSpawn('defaults read NSGlobalDomain InitialKeyRepeat'); + expect(parseInt(initialKeyRepeat.trim(), 10)).toBe(68); + }, + }, + validateDestroy: async () => { + const { data: keyRepeat } = await testSpawn('defaults read NSGlobalDomain KeyRepeat'); + const val = keyRepeat.trim(); + const parsed = parseInt(val, 10); + // Either deleted (error output or NaN) or reset to system default (6) + expect(val.includes('does not exist') || isNaN(parsed) || parsed === 6).toBe(true); + }, + }); + }); +});