mct-cli 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mct/__init__.py +5 -0
- mct/cli.py +224 -0
- mct/commands/dock.py +164 -0
- mct/commands/finder.py +153 -0
- mct/commands/keyboard.py +72 -0
- mct/commands/screenshot.py +145 -0
- mct/commands/system.py +238 -0
- mct/config.py +428 -0
- mct/defaults.py +124 -0
- mct_cli-0.2.3.dist-info/METADATA +139 -0
- mct_cli-0.2.3.dist-info/RECORD +14 -0
- mct_cli-0.2.3.dist-info/WHEEL +4 -0
- mct_cli-0.2.3.dist-info/entry_points.txt +2 -0
- mct_cli-0.2.3.dist-info/licenses/LICENSE +21 -0
mct/config.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Configuration management for declarative macOS settings."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from . import defaults
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
CONFIG_PATH = Path.home() / ".config" / "mct" / "config.yaml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Setting:
|
|
17
|
+
"""Represents a macOS setting that can be read/written."""
|
|
18
|
+
|
|
19
|
+
domain: str
|
|
20
|
+
key: str
|
|
21
|
+
value_type: str # 'bool', 'int', 'float', 'string'
|
|
22
|
+
restart_app: str | None = None # App to restart after changing
|
|
23
|
+
description: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Registry of all supported settings
|
|
27
|
+
# Format: config_key -> Setting
|
|
28
|
+
SETTINGS: dict[str, Setting] = {
|
|
29
|
+
# Dock settings
|
|
30
|
+
"dock.size": Setting(
|
|
31
|
+
domain="com.apple.dock",
|
|
32
|
+
key="tilesize",
|
|
33
|
+
value_type="int",
|
|
34
|
+
restart_app="Dock",
|
|
35
|
+
description="Dock icon size (32-128)",
|
|
36
|
+
),
|
|
37
|
+
"dock.autohide": Setting(
|
|
38
|
+
domain="com.apple.dock",
|
|
39
|
+
key="autohide",
|
|
40
|
+
value_type="bool",
|
|
41
|
+
restart_app="Dock",
|
|
42
|
+
description="Auto-hide the Dock",
|
|
43
|
+
),
|
|
44
|
+
"dock.size_immutable": Setting(
|
|
45
|
+
domain="com.apple.dock",
|
|
46
|
+
key="size-immutable",
|
|
47
|
+
value_type="bool",
|
|
48
|
+
restart_app="Dock",
|
|
49
|
+
description="Lock Dock size",
|
|
50
|
+
),
|
|
51
|
+
"dock.magnification": Setting(
|
|
52
|
+
domain="com.apple.dock",
|
|
53
|
+
key="magnification",
|
|
54
|
+
value_type="bool",
|
|
55
|
+
restart_app="Dock",
|
|
56
|
+
description="Enable Dock magnification",
|
|
57
|
+
),
|
|
58
|
+
"dock.largesize": Setting(
|
|
59
|
+
domain="com.apple.dock",
|
|
60
|
+
key="largesize",
|
|
61
|
+
value_type="int",
|
|
62
|
+
restart_app="Dock",
|
|
63
|
+
description="Magnified icon size (16-128)",
|
|
64
|
+
),
|
|
65
|
+
"dock.orientation": Setting(
|
|
66
|
+
domain="com.apple.dock",
|
|
67
|
+
key="orientation",
|
|
68
|
+
value_type="string",
|
|
69
|
+
restart_app="Dock",
|
|
70
|
+
description="Dock position: left, bottom, right",
|
|
71
|
+
),
|
|
72
|
+
"dock.mineffect": Setting(
|
|
73
|
+
domain="com.apple.dock",
|
|
74
|
+
key="mineffect",
|
|
75
|
+
value_type="string",
|
|
76
|
+
restart_app="Dock",
|
|
77
|
+
description="Minimize effect: genie, scale, suck",
|
|
78
|
+
),
|
|
79
|
+
"dock.minimize_to_application": Setting(
|
|
80
|
+
domain="com.apple.dock",
|
|
81
|
+
key="minimize-to-application",
|
|
82
|
+
value_type="bool",
|
|
83
|
+
restart_app="Dock",
|
|
84
|
+
description="Minimize windows into application icon",
|
|
85
|
+
),
|
|
86
|
+
"dock.show_recents": Setting(
|
|
87
|
+
domain="com.apple.dock",
|
|
88
|
+
key="show-recents",
|
|
89
|
+
value_type="bool",
|
|
90
|
+
restart_app="Dock",
|
|
91
|
+
description="Show recent applications in Dock",
|
|
92
|
+
),
|
|
93
|
+
"dock.static_only": Setting(
|
|
94
|
+
domain="com.apple.dock",
|
|
95
|
+
key="static-only",
|
|
96
|
+
value_type="bool",
|
|
97
|
+
restart_app="Dock",
|
|
98
|
+
description="Show only open applications",
|
|
99
|
+
),
|
|
100
|
+
# Finder settings
|
|
101
|
+
"finder.show_extensions": Setting(
|
|
102
|
+
domain="NSGlobalDomain",
|
|
103
|
+
key="AppleShowAllExtensions",
|
|
104
|
+
value_type="bool",
|
|
105
|
+
restart_app="Finder",
|
|
106
|
+
description="Show all file extensions",
|
|
107
|
+
),
|
|
108
|
+
"finder.show_hidden": Setting(
|
|
109
|
+
domain="com.apple.finder",
|
|
110
|
+
key="AppleShowAllFiles",
|
|
111
|
+
value_type="bool",
|
|
112
|
+
restart_app="Finder",
|
|
113
|
+
description="Show hidden files",
|
|
114
|
+
),
|
|
115
|
+
"finder.show_path_bar": Setting(
|
|
116
|
+
domain="com.apple.finder",
|
|
117
|
+
key="ShowPathbar",
|
|
118
|
+
value_type="bool",
|
|
119
|
+
restart_app="Finder",
|
|
120
|
+
description="Show path bar at bottom",
|
|
121
|
+
),
|
|
122
|
+
"finder.show_status_bar": Setting(
|
|
123
|
+
domain="com.apple.finder",
|
|
124
|
+
key="ShowStatusBar",
|
|
125
|
+
value_type="bool",
|
|
126
|
+
restart_app="Finder",
|
|
127
|
+
description="Show status bar at bottom",
|
|
128
|
+
),
|
|
129
|
+
"finder.default_view": Setting(
|
|
130
|
+
domain="com.apple.finder",
|
|
131
|
+
key="FXPreferredViewStyle",
|
|
132
|
+
value_type="string",
|
|
133
|
+
restart_app="Finder",
|
|
134
|
+
description="Default view: icnv, Nlsv, clmv, glyv",
|
|
135
|
+
),
|
|
136
|
+
"finder.search_scope": Setting(
|
|
137
|
+
domain="com.apple.finder",
|
|
138
|
+
key="FXDefaultSearchScope",
|
|
139
|
+
value_type="string",
|
|
140
|
+
restart_app="Finder",
|
|
141
|
+
description="Search scope: SCcf (current folder), SCsp (previous scope), SCev (entire Mac)",
|
|
142
|
+
),
|
|
143
|
+
"finder.empty_trash_warning": Setting(
|
|
144
|
+
domain="com.apple.finder",
|
|
145
|
+
key="WarnOnEmptyTrash",
|
|
146
|
+
value_type="bool",
|
|
147
|
+
restart_app="Finder",
|
|
148
|
+
description="Warn before emptying trash",
|
|
149
|
+
),
|
|
150
|
+
"finder.new_window_target": Setting(
|
|
151
|
+
domain="com.apple.finder",
|
|
152
|
+
key="NewWindowTarget",
|
|
153
|
+
value_type="string",
|
|
154
|
+
restart_app="Finder",
|
|
155
|
+
description="New window target: PfHm (Home), PfDe (Desktop), PfDo (Documents), PfLo (other)",
|
|
156
|
+
),
|
|
157
|
+
# Screenshot settings
|
|
158
|
+
"screenshot.location": Setting(
|
|
159
|
+
domain="com.apple.screencapture",
|
|
160
|
+
key="location",
|
|
161
|
+
value_type="string",
|
|
162
|
+
restart_app="SystemUIServer",
|
|
163
|
+
description="Screenshot save location",
|
|
164
|
+
),
|
|
165
|
+
"screenshot.format": Setting(
|
|
166
|
+
domain="com.apple.screencapture",
|
|
167
|
+
key="type",
|
|
168
|
+
value_type="string",
|
|
169
|
+
restart_app="SystemUIServer",
|
|
170
|
+
description="Screenshot format: png, jpg, gif, pdf, tiff",
|
|
171
|
+
),
|
|
172
|
+
"screenshot.disable_shadow": Setting(
|
|
173
|
+
domain="com.apple.screencapture",
|
|
174
|
+
key="disable-shadow",
|
|
175
|
+
value_type="bool",
|
|
176
|
+
restart_app="SystemUIServer",
|
|
177
|
+
description="Disable window shadow in screenshots",
|
|
178
|
+
),
|
|
179
|
+
"screenshot.include_date": Setting(
|
|
180
|
+
domain="com.apple.screencapture",
|
|
181
|
+
key="include-date",
|
|
182
|
+
value_type="bool",
|
|
183
|
+
restart_app="SystemUIServer",
|
|
184
|
+
description="Include date in screenshot filename",
|
|
185
|
+
),
|
|
186
|
+
"screenshot.show_thumbnail": Setting(
|
|
187
|
+
domain="com.apple.screencapture",
|
|
188
|
+
key="show-thumbnail",
|
|
189
|
+
value_type="bool",
|
|
190
|
+
restart_app="SystemUIServer",
|
|
191
|
+
description="Show floating thumbnail after capture",
|
|
192
|
+
),
|
|
193
|
+
# Keyboard settings
|
|
194
|
+
"keyboard.press_and_hold": Setting(
|
|
195
|
+
domain="NSGlobalDomain",
|
|
196
|
+
key="ApplePressAndHoldEnabled",
|
|
197
|
+
value_type="bool",
|
|
198
|
+
restart_app=None,
|
|
199
|
+
description="Enable press-and-hold for accents (false = key repeat)",
|
|
200
|
+
),
|
|
201
|
+
"keyboard.key_repeat_rate": Setting(
|
|
202
|
+
domain="NSGlobalDomain",
|
|
203
|
+
key="KeyRepeat",
|
|
204
|
+
value_type="int",
|
|
205
|
+
restart_app=None,
|
|
206
|
+
description="Key repeat rate (lower = faster, 1-15)",
|
|
207
|
+
),
|
|
208
|
+
"keyboard.initial_key_repeat": Setting(
|
|
209
|
+
domain="NSGlobalDomain",
|
|
210
|
+
key="InitialKeyRepeat",
|
|
211
|
+
value_type="int",
|
|
212
|
+
restart_app=None,
|
|
213
|
+
description="Delay before key repeat starts (lower = faster, 10-120)",
|
|
214
|
+
),
|
|
215
|
+
# Trackpad settings
|
|
216
|
+
"trackpad.tap_to_click": Setting(
|
|
217
|
+
domain="com.apple.AppleMultitouchTrackpad",
|
|
218
|
+
key="Clicking",
|
|
219
|
+
value_type="bool",
|
|
220
|
+
restart_app=None,
|
|
221
|
+
description="Enable tap to click",
|
|
222
|
+
),
|
|
223
|
+
"trackpad.natural_scrolling": Setting(
|
|
224
|
+
domain="NSGlobalDomain",
|
|
225
|
+
key="com.apple.swipescrolldirection",
|
|
226
|
+
value_type="bool",
|
|
227
|
+
restart_app=None,
|
|
228
|
+
description="Natural scrolling direction",
|
|
229
|
+
),
|
|
230
|
+
"trackpad.tracking_speed": Setting(
|
|
231
|
+
domain="NSGlobalDomain",
|
|
232
|
+
key="com.apple.trackpad.scaling",
|
|
233
|
+
value_type="float",
|
|
234
|
+
restart_app=None,
|
|
235
|
+
description="Tracking speed (0.0-3.0)",
|
|
236
|
+
),
|
|
237
|
+
# Menu bar settings
|
|
238
|
+
"menubar.autohide": Setting(
|
|
239
|
+
domain="NSGlobalDomain",
|
|
240
|
+
key="_HIHideMenuBar",
|
|
241
|
+
value_type="bool",
|
|
242
|
+
restart_app="SystemUIServer",
|
|
243
|
+
description="Auto-hide menu bar",
|
|
244
|
+
),
|
|
245
|
+
"menubar.show_background": Setting(
|
|
246
|
+
domain="NSGlobalDomain",
|
|
247
|
+
key="NSStatusBarShowsMenuBarBackground",
|
|
248
|
+
value_type="bool",
|
|
249
|
+
restart_app="SystemUIServer",
|
|
250
|
+
description="Show menu bar background (Tahoe)",
|
|
251
|
+
),
|
|
252
|
+
# Mission Control settings
|
|
253
|
+
"mission_control.auto_rearrange": Setting(
|
|
254
|
+
domain="com.apple.dock",
|
|
255
|
+
key="mru-spaces",
|
|
256
|
+
value_type="bool",
|
|
257
|
+
restart_app="Dock",
|
|
258
|
+
description="Automatically rearrange Spaces based on recent use",
|
|
259
|
+
),
|
|
260
|
+
"mission_control.group_by_app": Setting(
|
|
261
|
+
domain="com.apple.dock",
|
|
262
|
+
key="expose-group-apps",
|
|
263
|
+
value_type="bool",
|
|
264
|
+
restart_app="Dock",
|
|
265
|
+
description="Group windows by application",
|
|
266
|
+
),
|
|
267
|
+
# Accessibility settings
|
|
268
|
+
"accessibility.reduce_transparency": Setting(
|
|
269
|
+
domain="com.apple.universalaccess",
|
|
270
|
+
key="reduceTransparency",
|
|
271
|
+
value_type="bool",
|
|
272
|
+
restart_app=None,
|
|
273
|
+
description="Reduce transparency (helps with Liquid Glass)",
|
|
274
|
+
),
|
|
275
|
+
"accessibility.reduce_motion": Setting(
|
|
276
|
+
domain="com.apple.universalaccess",
|
|
277
|
+
key="reduceMotion",
|
|
278
|
+
value_type="bool",
|
|
279
|
+
restart_app=None,
|
|
280
|
+
description="Reduce motion effects",
|
|
281
|
+
),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class ConfigDiff:
|
|
287
|
+
"""Represents differences between current state and config."""
|
|
288
|
+
|
|
289
|
+
key: str
|
|
290
|
+
current: Any
|
|
291
|
+
desired: Any
|
|
292
|
+
setting: Setting
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def load_config() -> dict[str, Any]:
|
|
296
|
+
"""Load configuration from YAML file.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Nested dict of configuration values
|
|
300
|
+
"""
|
|
301
|
+
if not CONFIG_PATH.exists():
|
|
302
|
+
return {}
|
|
303
|
+
|
|
304
|
+
with open(CONFIG_PATH) as f:
|
|
305
|
+
return yaml.safe_load(f) or {}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def save_config(config: dict[str, Any]) -> None:
|
|
309
|
+
"""Save configuration to YAML file."""
|
|
310
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
311
|
+
with open(CONFIG_PATH, "w") as f:
|
|
312
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def flatten_config(config: dict[str, Any], prefix: str = "") -> dict[str, Any]:
|
|
316
|
+
"""Flatten nested config dict to dot-notation keys.
|
|
317
|
+
|
|
318
|
+
Example: {'dock': {'size': 48}} -> {'dock.size': 48}
|
|
319
|
+
"""
|
|
320
|
+
result = {}
|
|
321
|
+
for key, value in config.items():
|
|
322
|
+
full_key = f"{prefix}.{key}" if prefix else key
|
|
323
|
+
if isinstance(value, dict):
|
|
324
|
+
result.update(flatten_config(value, full_key))
|
|
325
|
+
else:
|
|
326
|
+
result[full_key] = value
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def unflatten_config(flat: dict[str, Any]) -> dict[str, Any]:
|
|
331
|
+
"""Unflatten dot-notation keys to nested dict.
|
|
332
|
+
|
|
333
|
+
Example: {'dock.size': 48} -> {'dock': {'size': 48}}
|
|
334
|
+
"""
|
|
335
|
+
result: dict[str, Any] = {}
|
|
336
|
+
for key, value in flat.items():
|
|
337
|
+
parts = key.split(".")
|
|
338
|
+
current = result
|
|
339
|
+
for part in parts[:-1]:
|
|
340
|
+
if part not in current:
|
|
341
|
+
current[part] = {}
|
|
342
|
+
current = current[part]
|
|
343
|
+
current[parts[-1]] = value
|
|
344
|
+
return result
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def read_current_state() -> dict[str, Any]:
|
|
348
|
+
"""Read all supported settings from the system."""
|
|
349
|
+
state = {}
|
|
350
|
+
for key, setting in SETTINGS.items():
|
|
351
|
+
value = defaults.read(setting.domain, setting.key)
|
|
352
|
+
if value is not None:
|
|
353
|
+
# Convert to proper type based on setting definition
|
|
354
|
+
if setting.value_type == "bool":
|
|
355
|
+
# macOS returns 0/1 for bools
|
|
356
|
+
value = bool(value) if isinstance(value, int) else value
|
|
357
|
+
state[key] = value
|
|
358
|
+
return state
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def compute_diff(config: dict[str, Any]) -> list[ConfigDiff]:
|
|
362
|
+
"""Compute differences between config and current system state.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
config: Flattened config dict
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
List of ConfigDiff for settings that differ
|
|
369
|
+
"""
|
|
370
|
+
diffs = []
|
|
371
|
+
current_state = read_current_state()
|
|
372
|
+
|
|
373
|
+
for key, desired in config.items():
|
|
374
|
+
if key not in SETTINGS:
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
setting = SETTINGS[key]
|
|
378
|
+
current = current_state.get(key)
|
|
379
|
+
|
|
380
|
+
if current != desired:
|
|
381
|
+
diffs.append(
|
|
382
|
+
ConfigDiff(key=key, current=current, desired=desired, setting=setting)
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
return diffs
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def apply_setting(key: str, value: Any) -> str | None:
|
|
389
|
+
"""Apply a single setting.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
App name that needs restart, or None
|
|
393
|
+
"""
|
|
394
|
+
if key not in SETTINGS:
|
|
395
|
+
raise ValueError(f"Unknown setting: {key}")
|
|
396
|
+
|
|
397
|
+
setting = SETTINGS[key]
|
|
398
|
+
defaults.write(setting.domain, setting.key, value, setting.value_type)
|
|
399
|
+
return setting.restart_app
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def apply_config(config: dict[str, Any], dry_run: bool = False) -> list[ConfigDiff]:
|
|
403
|
+
"""Apply configuration to the system.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
config: Flattened config dict
|
|
407
|
+
dry_run: If True, don't actually apply changes
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
List of changes that were (or would be) applied
|
|
411
|
+
"""
|
|
412
|
+
diffs = compute_diff(config)
|
|
413
|
+
|
|
414
|
+
if dry_run:
|
|
415
|
+
return diffs
|
|
416
|
+
|
|
417
|
+
apps_to_restart: set[str] = set()
|
|
418
|
+
|
|
419
|
+
for diff in diffs:
|
|
420
|
+
restart_app = apply_setting(diff.key, diff.desired)
|
|
421
|
+
if restart_app:
|
|
422
|
+
apps_to_restart.add(restart_app)
|
|
423
|
+
|
|
424
|
+
# Restart affected apps
|
|
425
|
+
for app in apps_to_restart:
|
|
426
|
+
defaults.restart_app(app)
|
|
427
|
+
|
|
428
|
+
return diffs
|
mct/defaults.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Helper module for macOS defaults commands."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DefaultsError(Exception):
|
|
8
|
+
"""Error when reading/writing macOS defaults."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read(domain: str, key: str) -> Any:
|
|
14
|
+
"""Read a value from macOS defaults.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
domain: The defaults domain (e.g., 'com.apple.dock')
|
|
18
|
+
key: The key to read
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The value, or None if not found
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
DefaultsError: If there's an error reading the value
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
["defaults", "read", domain, key],
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
check=True,
|
|
32
|
+
)
|
|
33
|
+
value = result.stdout.strip()
|
|
34
|
+
|
|
35
|
+
# Try to parse as int
|
|
36
|
+
try:
|
|
37
|
+
return int(value)
|
|
38
|
+
except ValueError:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
# Try to parse as float
|
|
42
|
+
try:
|
|
43
|
+
return float(value)
|
|
44
|
+
except ValueError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# Handle boolean strings
|
|
48
|
+
if value in ("1", "true", "yes"):
|
|
49
|
+
return True
|
|
50
|
+
if value in ("0", "false", "no"):
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
return value
|
|
54
|
+
except subprocess.CalledProcessError:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_global(key: str) -> Any:
|
|
59
|
+
"""Read a value from global defaults (-g)."""
|
|
60
|
+
return read("-g", key)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write(domain: str, key: str, value: Any, value_type: str | None = None) -> None:
|
|
64
|
+
"""Write a value to macOS defaults.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
domain: The defaults domain (e.g., 'com.apple.dock')
|
|
68
|
+
key: The key to write
|
|
69
|
+
value: The value to write
|
|
70
|
+
value_type: Optional type hint ('bool', 'int', 'float', 'string')
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
DefaultsError: If there's an error writing the value
|
|
74
|
+
"""
|
|
75
|
+
cmd = ["defaults", "write", domain, key]
|
|
76
|
+
|
|
77
|
+
if value_type == "bool" or isinstance(value, bool):
|
|
78
|
+
cmd.extend(["-bool", "true" if value else "false"])
|
|
79
|
+
elif value_type == "int" or isinstance(value, int):
|
|
80
|
+
cmd.extend(["-int", str(value)])
|
|
81
|
+
elif value_type == "float" or isinstance(value, float):
|
|
82
|
+
cmd.extend(["-float", str(value)])
|
|
83
|
+
else:
|
|
84
|
+
cmd.append(str(value))
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
88
|
+
except subprocess.CalledProcessError as e:
|
|
89
|
+
raise DefaultsError(f"Failed to write {domain} {key}: {e.stderr}") from e
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def write_global(key: str, value: Any, value_type: str | None = None) -> None:
|
|
93
|
+
"""Write a value to global defaults (-g)."""
|
|
94
|
+
write("-g", key, value, value_type)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def delete(domain: str, key: str) -> None:
|
|
98
|
+
"""Delete a key from macOS defaults."""
|
|
99
|
+
try:
|
|
100
|
+
subprocess.run(
|
|
101
|
+
["defaults", "delete", domain, key],
|
|
102
|
+
check=True,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
)
|
|
106
|
+
except subprocess.CalledProcessError:
|
|
107
|
+
pass # Key might not exist
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def restart_app(app_name: str) -> None:
|
|
111
|
+
"""Restart an application to apply changes.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
app_name: The application name (e.g., 'Dock', 'Finder', 'SystemUIServer')
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
subprocess.run(
|
|
118
|
+
["killall", app_name],
|
|
119
|
+
check=True,
|
|
120
|
+
capture_output=True,
|
|
121
|
+
text=True,
|
|
122
|
+
)
|
|
123
|
+
except subprocess.CalledProcessError:
|
|
124
|
+
pass # App might not be running
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mct-cli
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: macOS Configuration Tools
|
|
5
|
+
Author-email: Oscar Colunga <oscar@ancile.dev>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Requires-Dist: typer>=0.9.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# macOS Configuration Tools (mct)
|
|
13
|
+
|
|
14
|
+
A personal collection of CLI tools for managing macOS settings through a simple, intuitive interface.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
Currently implemented:
|
|
19
|
+
|
|
20
|
+
### General
|
|
21
|
+
- Check version: `mct --version` or `mct -v` - Display the installed version
|
|
22
|
+
|
|
23
|
+
### Dock Management
|
|
24
|
+
- Set dock size: `mct dock size <value>` (32-128)
|
|
25
|
+
- Show current dock size: `mct dock size`
|
|
26
|
+
- Auto-hide controls:
|
|
27
|
+
- `mct dock hide` - Enable auto-hide
|
|
28
|
+
- `mct dock show` - Disable auto-hide
|
|
29
|
+
- Size lock controls:
|
|
30
|
+
- `mct dock lock` - Lock dock size
|
|
31
|
+
- `mct dock unlock` - Unlock dock size
|
|
32
|
+
- Reset options:
|
|
33
|
+
- `mct dock reset -s` - Reset size to default (64)
|
|
34
|
+
- `mct dock reset -h` - Reset auto-hide to default (disabled)
|
|
35
|
+
- `mct dock reset -l` - Reset size lock to default (unlocked)
|
|
36
|
+
- `mct dock reset -a` - Reset all dock settings
|
|
37
|
+
|
|
38
|
+
### Keyboard Management
|
|
39
|
+
- Key repeat controls:
|
|
40
|
+
- `mct keyboard hold` - Enable press-and-hold for accented characters
|
|
41
|
+
- `mct keyboard repeat` - Enable key repeat (disables accents)
|
|
42
|
+
- Reset options:
|
|
43
|
+
- `mct keyboard reset -h` - Reset key hold to default (enabled)
|
|
44
|
+
- `mct keyboard reset -a` - Reset all keyboard settings
|
|
45
|
+
|
|
46
|
+
### System Management
|
|
47
|
+
- Touch ID for sudo:
|
|
48
|
+
- `mct system touchid` - Enable Touch ID authentication for sudo with interactive backup management
|
|
49
|
+
- `mct system reset -t` - Reset Touch ID sudo configuration from backup
|
|
50
|
+
- `mct system reset -a` - Reset all system settings to defaults
|
|
51
|
+
|
|
52
|
+
Planned features:
|
|
53
|
+
- Configuration file support (`~/.config/mct/config.toml`) for:
|
|
54
|
+
- Setting default values for commands
|
|
55
|
+
- Storing preferred configurations
|
|
56
|
+
- Batch applying multiple settings at once
|
|
57
|
+
- Example configuration:
|
|
58
|
+
```toml
|
|
59
|
+
[dock]
|
|
60
|
+
default_size = 48
|
|
61
|
+
auto_hide = true
|
|
62
|
+
size_locked = false
|
|
63
|
+
|
|
64
|
+
[keyboard]
|
|
65
|
+
key_hold = true
|
|
66
|
+
|
|
67
|
+
[system]
|
|
68
|
+
touch_id_sudo = true
|
|
69
|
+
```
|
|
70
|
+
- More dock management options
|
|
71
|
+
- System preferences management
|
|
72
|
+
- And more...
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
### Using Homebrew (recommended)
|
|
77
|
+
```bash
|
|
78
|
+
# Add the tap repository
|
|
79
|
+
brew tap ocolunga/mct-cli
|
|
80
|
+
|
|
81
|
+
# Install mct-cli
|
|
82
|
+
brew install mct-cli
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Using pip
|
|
86
|
+
```bash
|
|
87
|
+
pip install mct-cli
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Using uv
|
|
91
|
+
```bash
|
|
92
|
+
uv tool install mct-cli
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### From source
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/ocolunga/mct.git
|
|
98
|
+
cd mct
|
|
99
|
+
uv sync
|
|
100
|
+
uv run mct --help
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Usage Examples
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Show help
|
|
107
|
+
mct --help
|
|
108
|
+
mct dock --help
|
|
109
|
+
mct keyboard --help
|
|
110
|
+
mct system --help
|
|
111
|
+
|
|
112
|
+
# Check version
|
|
113
|
+
mct --version
|
|
114
|
+
|
|
115
|
+
# Dock Examples
|
|
116
|
+
mct dock size 48 # Set dock size to 48
|
|
117
|
+
mct dock size # Show current dock size
|
|
118
|
+
mct dock hide # Enable auto-hide
|
|
119
|
+
mct dock show # Disable auto-hide
|
|
120
|
+
mct dock lock # Lock dock size
|
|
121
|
+
mct dock unlock # Unlock dock size
|
|
122
|
+
mct dock reset -s -h # Reset both size and auto-hide
|
|
123
|
+
|
|
124
|
+
# Keyboard Examples
|
|
125
|
+
mct keyboard hold # Enable press-and-hold for accents
|
|
126
|
+
mct keyboard repeat # Enable key repeat (disable accents)
|
|
127
|
+
mct keyboard reset -a # Reset all keyboard settings
|
|
128
|
+
|
|
129
|
+
# System Examples
|
|
130
|
+
mct system touchid # Enable Touch ID for sudo with interactive backup
|
|
131
|
+
mct system reset -t # Reset Touch ID sudo configuration from backup
|
|
132
|
+
mct system reset -a # Reset all system settings to defaults
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Note: Some commands may require restarting applications to take effect.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
mct/__init__.py,sha256=2XBV1zscKmG3c5detK3HeeI1fdhVxfUpku_d_3DgxXk,168
|
|
2
|
+
mct/cli.py,sha256=hqVp11SQzR7pyVFmj59jPnIRo3YkvUXMqHxIK7aiIt4,6616
|
|
3
|
+
mct/config.py,sha256=CQ8mMdKsAIZNJCHonpjCfVHEBGIAzm82A7VnADBNEjs,12675
|
|
4
|
+
mct/defaults.py,sha256=6wLX5ODd39k7t-PSo0XdYsMICoUJ31aekN0HwrTMWVM,3303
|
|
5
|
+
mct/commands/dock.py,sha256=tSZFtcAJInDoG6gvJRsslSMTaOdqRgCloLUTyM9GI9o,5124
|
|
6
|
+
mct/commands/finder.py,sha256=devM03j4VquMQGDQM24_or5le9_3_I-Q7GAKaph6wLQ,4990
|
|
7
|
+
mct/commands/keyboard.py,sha256=uKFKQlIE0dO9nwVq-YWVPGhdq7Mqp5E1pU-I19TLXm4,2414
|
|
8
|
+
mct/commands/screenshot.py,sha256=v1s_89sL7gX_KKt4uyjc0zKHgUO9XzItmfdRGrdZ8x8,4920
|
|
9
|
+
mct/commands/system.py,sha256=Zf7PlvwqY1Go3K4BCjSc6GIaOwfw5SaZ-yjjRNsCCjc,8351
|
|
10
|
+
mct_cli-0.2.3.dist-info/METADATA,sha256=TOmgnQtOEHgk0Nof6K5An7cUru2XDVfqh4gi6Bfrxbk,3644
|
|
11
|
+
mct_cli-0.2.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
+
mct_cli-0.2.3.dist-info/entry_points.txt,sha256=QiGrcxAWYra7JelqpSF1reNTg8fNMMrTMHRV5hb--lA,37
|
|
13
|
+
mct_cli-0.2.3.dist-info/licenses/LICENSE,sha256=FyuBsNr3YLXHPRQfWIpCAC4brUeSdYelr01V-Fkog8U,1066
|
|
14
|
+
mct_cli-0.2.3.dist-info/RECORD,,
|