From ded515ace7848f7443ba8fdcfc78897821619abc Mon Sep 17 00:00:00 2001 From: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Date: Thu, 14 May 2026 17:26:56 +0000 Subject: [PATCH 1/3] feat: Add macos-settings resource (auto-generated from issue #37) --- docs/resources/(resources)/macos-settings.mdx | 135 ++++++ src/index.ts | 2 + .../macos-settings/macos-settings-resource.ts | 454 ++++++++++++++++++ test/macos/macos-settings.test.ts | 136 ++++++ 4 files changed, 727 insertions(+) create mode 100644 docs/resources/(resources)/macos-settings.mdx create mode 100644 src/resources/macos/macos-settings/macos-settings-resource.ts create mode 100644 test/macos/macos-settings.test.ts 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..966a01f --- /dev/null +++ b/src/resources/macos/macos-settings/macos-settings-resource.ts @@ -0,0 +1,454 @@ +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: { canModify: true }, + keyboard: { canModify: true }, + trackpad: { canModify: true }, + dock: { canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const result: Partial = {}; + + if (parameters.mouse) { + result.mouse = await this.readMouseSettings(parameters.mouse); + } + if (parameters.keyboard) { + result.keyboard = await this.readKeyboardSettings(parameters.keyboard); + } + if (parameters.trackpad) { + result.trackpad = await this.readTrackpadSettings(parameters.trackpad); + } + if (parameters.dock) { + result.dock = await this.readDockSettings(parameters.dock); + } + + return result; + } + + 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 = {}; + + if ('naturalScrolling' in desired) { + result.naturalScrolling = await this.readBool('NSGlobalDomain', 'com.apple.swipescrolldirection') ?? true; + } + if ('acceleration' in desired) { + const linear = await this.readBool('NSGlobalDomain', 'com.apple.mouse.linear') ?? false; + // com.apple.mouse.linear=true means acceleration is DISABLED; invert for user-friendly name + result.acceleration = !linear; + } + if ('speed' in desired) { + result.speed = await this.readFloat('NSGlobalDomain', 'com.apple.mouse.scaling') ?? 1.5; + } + + return result; + } + + 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 = {}; + + if ('keyRepeat' in desired) { + result.keyRepeat = await this.readInt('NSGlobalDomain', 'KeyRepeat') ?? 6; + } + if ('initialKeyRepeat' in desired) { + result.initialKeyRepeat = await this.readInt('NSGlobalDomain', 'InitialKeyRepeat') ?? 68; + } + if ('pressAndHold' in desired) { + result.pressAndHold = await this.readBool('NSGlobalDomain', 'ApplePressAndHoldEnabled') ?? true; + } + if ('fnKeysAsStandardKeys' in desired) { + result.fnKeysAsStandardKeys = await this.readBool('NSGlobalDomain', 'com.apple.keyboard.fnState') ?? false; + } + if ('keyboardNavigation' in desired) { + const val = await this.readInt('NSGlobalDomain', 'AppleKeyboardUIMode') ?? 0; + // AppleKeyboardUIMode: 0=disabled, 2=enabled + result.keyboardNavigation = val === 2; + } + + return result; + } + + 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 = {}; + + if ('speed' in desired) { + result.speed = await this.readFloat('NSGlobalDomain', 'com.apple.trackpad.scaling') ?? 1.5; + } + + return result; + } + + 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 = {}; + + if ('position' in desired) { + result.position = (await this.readString('com.apple.dock', 'orientation') ?? 'bottom') as DockConfig['position']; + } + if ('iconSize' in desired) { + result.iconSize = await this.readInt('com.apple.dock', 'tilesize') ?? 48; + } + if ('autohide' in desired) { + result.autohide = await this.readBool('com.apple.dock', 'autohide') ?? false; + } + if ('autohideDelay' in desired) { + result.autohideDelay = await this.readFloat('com.apple.dock', 'autohide-delay') ?? 0.2; + } + if ('showRecents' in desired) { + result.showRecents = await this.readBool('com.apple.dock', 'show-recents') ?? true; + } + if ('minimizeEffect' in desired) { + result.minimizeEffect = (await this.readString('com.apple.dock', 'mineffect') ?? 'genie') as DockConfig['minimizeEffect']; + } + + return result; + } + + 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..f1f5dde --- /dev/null +++ b/test/macos/macos-settings.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, it } from 'vitest'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import * as path from 'node:path'; + +describe('macos-settings resource integration tests', 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 parsed = parseInt(tilesize.trim(), 10); + // Either deleted (NaN) or reset to system default (48) + expect(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 parsed = parseInt(keyRepeat.trim(), 10); + expect(isNaN(parsed) || parsed === 6).toBe(true); + }, + }); + }); +}); From d957be3aa771e97638185400cb240e8141d7d4f3 Mon Sep 17 00:00:00 2001 From: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Date: Thu, 14 May 2026 17:29:12 +0000 Subject: [PATCH 2/3] fix: Linux test fixes for macos-settings resource --- test/macos/macos-settings.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/macos/macos-settings.test.ts b/test/macos/macos-settings.test.ts index f1f5dde..18fcf9c 100644 --- a/test/macos/macos-settings.test.ts +++ b/test/macos/macos-settings.test.ts @@ -1,8 +1,9 @@ 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', async () => { +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 () => { From da917d2ed2e13d0367805967dd767582ccf94038 Mon Sep 17 00:00:00 2001 From: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Date: Thu, 14 May 2026 17:41:30 +0000 Subject: [PATCH 3/3] fix: macOS test fixes for macos-settings resource --- .../macos-settings/macos-settings-resource.ts | 90 ++++++++++++------- test/macos/macos-settings.test.ts | 13 +-- 2 files changed, 64 insertions(+), 39 deletions(-) diff --git a/src/resources/macos/macos-settings/macos-settings-resource.ts b/src/resources/macos/macos-settings/macos-settings-resource.ts index 966a01f..e158118 100644 --- a/src/resources/macos/macos-settings/macos-settings-resource.ts +++ b/src/resources/macos/macos-settings/macos-settings-resource.ts @@ -123,31 +123,36 @@ export class MacosSettingsResource extends Resource { example2: exampleNonAppleKeyboard, }, parameterSettings: { - mouse: { canModify: true }, - keyboard: { canModify: true }, - trackpad: { canModify: true }, - dock: { canModify: true }, + 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) { - result.mouse = await this.readMouseSettings(parameters.mouse); + const mouse = await this.readMouseSettings(parameters.mouse); + if (mouse !== null) { result.mouse = mouse; anyFound = true; } } if (parameters.keyboard) { - result.keyboard = await this.readKeyboardSettings(parameters.keyboard); + const keyboard = await this.readKeyboardSettings(parameters.keyboard); + if (keyboard !== null) { result.keyboard = keyboard; anyFound = true; } } if (parameters.trackpad) { - result.trackpad = await this.readTrackpadSettings(parameters.trackpad); + const trackpad = await this.readTrackpadSettings(parameters.trackpad); + if (trackpad !== null) { result.trackpad = trackpad; anyFound = true; } } if (parameters.dock) { - result.dock = await this.readDockSettings(parameters.dock); + const dock = await this.readDockSettings(parameters.dock); + if (dock !== null) { result.dock = dock; anyFound = true; } } - return result; + return anyFound ? result : null; } override async create(plan: CreatePlan): Promise { @@ -207,22 +212,25 @@ export class MacosSettingsResource extends Resource { // ---- Mouse ---- - private async readMouseSettings(desired: MouseConfig): Promise { + private async readMouseSettings(desired: MouseConfig): Promise { const result: MouseConfig = {}; + let anyFound = false; if ('naturalScrolling' in desired) { - result.naturalScrolling = await this.readBool('NSGlobalDomain', 'com.apple.swipescrolldirection') ?? true; + 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') ?? false; + const linear = await this.readBool('NSGlobalDomain', 'com.apple.mouse.linear'); // com.apple.mouse.linear=true means acceleration is DISABLED; invert for user-friendly name - result.acceleration = !linear; + if (linear !== null) { result.acceleration = !linear; anyFound = true; } } if ('speed' in desired) { - result.speed = await this.readFloat('NSGlobalDomain', 'com.apple.mouse.scaling') ?? 1.5; + const v = await this.readFloat('NSGlobalDomain', 'com.apple.mouse.scaling'); + if (v !== null) { result.speed = v; anyFound = true; } } - return result; + return anyFound ? result : null; } private async applyMouseSettings(settings: MouseConfig): Promise { @@ -256,28 +264,33 @@ export class MacosSettingsResource extends Resource { // ---- Keyboard ---- - private async readKeyboardSettings(desired: KeyboardConfig): Promise { + private async readKeyboardSettings(desired: KeyboardConfig): Promise { const result: KeyboardConfig = {}; + let anyFound = false; if ('keyRepeat' in desired) { - result.keyRepeat = await this.readInt('NSGlobalDomain', 'KeyRepeat') ?? 6; + const v = await this.readInt('NSGlobalDomain', 'KeyRepeat'); + if (v !== null) { result.keyRepeat = v; anyFound = true; } } if ('initialKeyRepeat' in desired) { - result.initialKeyRepeat = await this.readInt('NSGlobalDomain', 'InitialKeyRepeat') ?? 68; + const v = await this.readInt('NSGlobalDomain', 'InitialKeyRepeat'); + if (v !== null) { result.initialKeyRepeat = v; anyFound = true; } } if ('pressAndHold' in desired) { - result.pressAndHold = await this.readBool('NSGlobalDomain', 'ApplePressAndHoldEnabled') ?? true; + const v = await this.readBool('NSGlobalDomain', 'ApplePressAndHoldEnabled'); + if (v !== null) { result.pressAndHold = v; anyFound = true; } } if ('fnKeysAsStandardKeys' in desired) { - result.fnKeysAsStandardKeys = await this.readBool('NSGlobalDomain', 'com.apple.keyboard.fnState') ?? false; + const v = await this.readBool('NSGlobalDomain', 'com.apple.keyboard.fnState'); + if (v !== null) { result.fnKeysAsStandardKeys = v; anyFound = true; } } if ('keyboardNavigation' in desired) { - const val = await this.readInt('NSGlobalDomain', 'AppleKeyboardUIMode') ?? 0; + const v = await this.readInt('NSGlobalDomain', 'AppleKeyboardUIMode'); // AppleKeyboardUIMode: 0=disabled, 2=enabled - result.keyboardNavigation = val === 2; + if (v !== null) { result.keyboardNavigation = v === 2; anyFound = true; } } - return result; + return anyFound ? result : null; } private async applyKeyboardSettings(settings: KeyboardConfig): Promise { @@ -323,14 +336,16 @@ export class MacosSettingsResource extends Resource { // ---- Trackpad ---- - private async readTrackpadSettings(desired: TrackpadConfig): Promise { + private async readTrackpadSettings(desired: TrackpadConfig): Promise { const result: TrackpadConfig = {}; + let anyFound = false; if ('speed' in desired) { - result.speed = await this.readFloat('NSGlobalDomain', 'com.apple.trackpad.scaling') ?? 1.5; + const v = await this.readFloat('NSGlobalDomain', 'com.apple.trackpad.scaling'); + if (v !== null) { result.speed = v; anyFound = true; } } - return result; + return anyFound ? result : null; } private async applyTrackpadSettings(settings: TrackpadConfig): Promise { @@ -351,29 +366,36 @@ export class MacosSettingsResource extends Resource { // ---- Dock ---- - private async readDockSettings(desired: DockConfig): Promise { + private async readDockSettings(desired: DockConfig): Promise { const result: DockConfig = {}; + let anyFound = false; if ('position' in desired) { - result.position = (await this.readString('com.apple.dock', 'orientation') ?? 'bottom') as DockConfig['position']; + const v = await this.readString('com.apple.dock', 'orientation'); + if (v !== null) { result.position = v as DockConfig['position']; anyFound = true; } } if ('iconSize' in desired) { - result.iconSize = await this.readInt('com.apple.dock', 'tilesize') ?? 48; + const v = await this.readInt('com.apple.dock', 'tilesize'); + if (v !== null) { result.iconSize = v; anyFound = true; } } if ('autohide' in desired) { - result.autohide = await this.readBool('com.apple.dock', 'autohide') ?? false; + const v = await this.readBool('com.apple.dock', 'autohide'); + if (v !== null) { result.autohide = v; anyFound = true; } } if ('autohideDelay' in desired) { - result.autohideDelay = await this.readFloat('com.apple.dock', 'autohide-delay') ?? 0.2; + const v = await this.readFloat('com.apple.dock', 'autohide-delay'); + if (v !== null) { result.autohideDelay = v; anyFound = true; } } if ('showRecents' in desired) { - result.showRecents = await this.readBool('com.apple.dock', 'show-recents') ?? true; + const v = await this.readBool('com.apple.dock', 'show-recents'); + if (v !== null) { result.showRecents = v; anyFound = true; } } if ('minimizeEffect' in desired) { - result.minimizeEffect = (await this.readString('com.apple.dock', 'mineffect') ?? 'genie') as DockConfig['minimizeEffect']; + const v = await this.readString('com.apple.dock', 'mineffect'); + if (v !== null) { result.minimizeEffect = v as DockConfig['minimizeEffect']; anyFound = true; } } - return result; + return anyFound ? result : null; } private async applyDockSettings(settings: DockConfig): Promise { diff --git a/test/macos/macos-settings.test.ts b/test/macos/macos-settings.test.ts index 18fcf9c..01be39b 100644 --- a/test/macos/macos-settings.test.ts +++ b/test/macos/macos-settings.test.ts @@ -82,9 +82,10 @@ describe('macos-settings resource integration tests', { skip: !Utils.isMacOS() } 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 parsed = parseInt(tilesize.trim(), 10); - // Either deleted (NaN) or reset to system default (48) - expect(isNaN(parsed) || parsed === 48).toBe(true); + 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); }, }); }); @@ -129,8 +130,10 @@ describe('macos-settings resource integration tests', { skip: !Utils.isMacOS() } }, validateDestroy: async () => { const { data: keyRepeat } = await testSpawn('defaults read NSGlobalDomain KeyRepeat'); - const parsed = parseInt(keyRepeat.trim(), 10); - expect(isNaN(parsed) || parsed === 6).toBe(true); + 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); }, }); });