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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Screenshot settings management."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ..defaults import read, write, restart_app
|
|
9
|
+
|
|
10
|
+
screenshot_app = typer.Typer()
|
|
11
|
+
|
|
12
|
+
ON_VALUES = ("on", "true", "1", "yes")
|
|
13
|
+
OFF_VALUES = ("off", "false", "0", "no")
|
|
14
|
+
FORMATS = ("png", "jpg", "gif", "pdf", "tiff")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_bool(value: str) -> bool | None:
|
|
18
|
+
"""Parse a boolean string value."""
|
|
19
|
+
if value.lower() in ON_VALUES:
|
|
20
|
+
return True
|
|
21
|
+
if value.lower() in OFF_VALUES:
|
|
22
|
+
return False
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@screenshot_app.command()
|
|
27
|
+
def location(path: Optional[str] = typer.Argument(None, help="Directory path")):
|
|
28
|
+
"""Get or set screenshot save location."""
|
|
29
|
+
if path is None:
|
|
30
|
+
current = read("com.apple.screencapture", "location")
|
|
31
|
+
typer.echo(current if current else "~/Desktop (default)")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
expanded = Path(path).expanduser().resolve()
|
|
35
|
+
|
|
36
|
+
if not expanded.exists():
|
|
37
|
+
typer.echo(f"Error: directory does not exist: {expanded}")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
if not expanded.is_dir():
|
|
41
|
+
typer.echo(f"Error: not a directory: {expanded}")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
|
|
44
|
+
write("com.apple.screencapture", "location", str(expanded), "string")
|
|
45
|
+
restart_app("SystemUIServer")
|
|
46
|
+
typer.echo(f"Screenshot location set to {expanded}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@screenshot_app.command()
|
|
50
|
+
def format(fmt: Optional[str] = typer.Argument(None, help="png/jpg/gif/pdf/tiff")):
|
|
51
|
+
"""Get or set screenshot file format."""
|
|
52
|
+
if fmt is None:
|
|
53
|
+
current = read("com.apple.screencapture", "type")
|
|
54
|
+
typer.echo(current if current else "png (default)")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if fmt.lower() not in FORMATS:
|
|
58
|
+
typer.echo(f"Error: use {', '.join(FORMATS)}")
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
write("com.apple.screencapture", "type", fmt.lower(), "string")
|
|
62
|
+
restart_app("SystemUIServer")
|
|
63
|
+
typer.echo(f"Screenshot format set to {fmt.lower()}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@screenshot_app.command()
|
|
67
|
+
def shadow(value: Optional[str] = typer.Argument(None, help="on/off")):
|
|
68
|
+
"""Get or set window shadow in screenshots."""
|
|
69
|
+
if value is None:
|
|
70
|
+
# disable-shadow=true means shadow is OFF
|
|
71
|
+
disabled = read("com.apple.screencapture", "disable-shadow")
|
|
72
|
+
typer.echo("off" if disabled else "on")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
parsed = parse_bool(value)
|
|
76
|
+
if parsed is None:
|
|
77
|
+
typer.echo("Error: use 'on' or 'off'")
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
|
|
80
|
+
# Invert: shadow on = disable-shadow false
|
|
81
|
+
write("com.apple.screencapture", "disable-shadow", not parsed, "bool")
|
|
82
|
+
restart_app("SystemUIServer")
|
|
83
|
+
typer.echo(f"Window shadow {'enabled' if parsed else 'disabled'}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@screenshot_app.command()
|
|
87
|
+
def thumbnail(value: Optional[str] = typer.Argument(None, help="on/off")):
|
|
88
|
+
"""Get or set floating thumbnail after capture."""
|
|
89
|
+
if value is None:
|
|
90
|
+
current = read("com.apple.screencapture", "show-thumbnail")
|
|
91
|
+
# Default is on if not set
|
|
92
|
+
typer.echo("on" if current is None or current else "off")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
parsed = parse_bool(value)
|
|
96
|
+
if parsed is None:
|
|
97
|
+
typer.echo("Error: use 'on' or 'off'")
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
write("com.apple.screencapture", "show-thumbnail", parsed, "bool")
|
|
101
|
+
restart_app("SystemUIServer")
|
|
102
|
+
typer.echo(f"Floating thumbnail {'enabled' if parsed else 'disabled'}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
SETTINGS_MAP = {
|
|
106
|
+
"location": ("com.apple.screencapture", "location", "string", str(Path.home() / "Desktop")),
|
|
107
|
+
"format": ("com.apple.screencapture", "type", "string", "png"),
|
|
108
|
+
"shadow": ("com.apple.screencapture", "disable-shadow", "bool", False), # False = shadow ON
|
|
109
|
+
"thumbnail": ("com.apple.screencapture", "show-thumbnail", "bool", True),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@screenshot_app.command()
|
|
114
|
+
def reset(setting: Optional[str] = typer.Argument(None, help="Setting to reset (or omit for all)")):
|
|
115
|
+
"""Reset screenshot settings to macOS defaults."""
|
|
116
|
+
if setting is None:
|
|
117
|
+
for name, (domain, key, vtype, default) in SETTINGS_MAP.items():
|
|
118
|
+
write(domain, key, default, vtype)
|
|
119
|
+
if name == "shadow":
|
|
120
|
+
display = "on" # disable-shadow=false means shadow is ON
|
|
121
|
+
elif vtype == "bool":
|
|
122
|
+
display = "on" if default else "off"
|
|
123
|
+
else:
|
|
124
|
+
display = default
|
|
125
|
+
typer.echo(f" {name}: reset to {display}")
|
|
126
|
+
restart_app("SystemUIServer")
|
|
127
|
+
typer.echo("All screenshot settings reset")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
if setting not in SETTINGS_MAP:
|
|
131
|
+
typer.echo(f"Error: unknown setting '{setting}'")
|
|
132
|
+
typer.echo(f"Available: {', '.join(SETTINGS_MAP.keys())}")
|
|
133
|
+
raise typer.Exit(1)
|
|
134
|
+
|
|
135
|
+
domain, key, vtype, default = SETTINGS_MAP[setting]
|
|
136
|
+
write(domain, key, default, vtype)
|
|
137
|
+
restart_app("SystemUIServer")
|
|
138
|
+
|
|
139
|
+
if setting == "shadow":
|
|
140
|
+
display = "on"
|
|
141
|
+
elif vtype == "bool":
|
|
142
|
+
display = "on" if default else "off"
|
|
143
|
+
else:
|
|
144
|
+
display = default
|
|
145
|
+
typer.echo(f"Screenshot {setting} reset to {display}")
|
mct/commands/system.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
system_app = typer.Typer()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def print_file_contents(file_path):
|
|
8
|
+
"""Print the contents of a file."""
|
|
9
|
+
try:
|
|
10
|
+
result = subprocess.run(
|
|
11
|
+
["sudo", "cat", file_path],
|
|
12
|
+
capture_output=True,
|
|
13
|
+
text=True,
|
|
14
|
+
check=True,
|
|
15
|
+
)
|
|
16
|
+
typer.echo("\nFile contents:")
|
|
17
|
+
typer.echo("=" * 50)
|
|
18
|
+
typer.echo(result.stdout)
|
|
19
|
+
typer.echo("=" * 50)
|
|
20
|
+
except subprocess.CalledProcessError as e:
|
|
21
|
+
typer.echo(f"Error reading file: {e}", err=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@system_app.command()
|
|
25
|
+
def touchid():
|
|
26
|
+
"""Enable Touch ID authentication for sudo commands."""
|
|
27
|
+
try:
|
|
28
|
+
# Show initial warning and get confirmation
|
|
29
|
+
typer.echo("\n⚠️ This operation will:")
|
|
30
|
+
typer.echo(" 1. Check if Touch ID is already enabled")
|
|
31
|
+
typer.echo(
|
|
32
|
+
" 2. Add a new authentication line to /etc/pam.d/sudo if needed"
|
|
33
|
+
)
|
|
34
|
+
typer.echo(" 3. Create or update a backup of the original file")
|
|
35
|
+
typer.echo(" 4. Require sudo privileges to make these changes")
|
|
36
|
+
|
|
37
|
+
if not typer.confirm("\nDo you want to proceed?", default=False):
|
|
38
|
+
typer.echo("Operation cancelled")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Check if the line already exists
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
["grep", "auth sufficient pam_tid.so", "/etc/pam.d/sudo"],
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if result.returncode == 0:
|
|
49
|
+
typer.echo("Touch ID for sudo is already enabled")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Check if backup exists
|
|
53
|
+
backup_exists = (
|
|
54
|
+
subprocess.run(
|
|
55
|
+
["test", "-f", "/etc/pam.d/sudo.bak"],
|
|
56
|
+
capture_output=True,
|
|
57
|
+
).returncode
|
|
58
|
+
== 0
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if backup_exists:
|
|
62
|
+
while True:
|
|
63
|
+
typer.echo(
|
|
64
|
+
"\n⚠️ A backup file already exists at /etc/pam.d/sudo.bak"
|
|
65
|
+
)
|
|
66
|
+
typer.echo("\nPlease choose an option:")
|
|
67
|
+
typer.echo("0 - Do nothing and exit")
|
|
68
|
+
typer.echo("1 - View the backup file contents")
|
|
69
|
+
typer.echo("2 - Continue (will replace existing backup)")
|
|
70
|
+
typer.echo("3 - Restore backup and then enable Touch ID")
|
|
71
|
+
|
|
72
|
+
choice = typer.prompt(
|
|
73
|
+
"\nEnter your choice (0-3)", type=int, default=0
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if choice == 0:
|
|
77
|
+
typer.echo("Operation cancelled")
|
|
78
|
+
return
|
|
79
|
+
elif choice == 1:
|
|
80
|
+
print_file_contents("/etc/pam.d/sudo.bak")
|
|
81
|
+
continue
|
|
82
|
+
elif choice == 2:
|
|
83
|
+
# Create a new backup of the current file
|
|
84
|
+
subprocess.run(
|
|
85
|
+
[
|
|
86
|
+
"sudo",
|
|
87
|
+
"cp",
|
|
88
|
+
"/etc/pam.d/sudo",
|
|
89
|
+
"/etc/pam.d/sudo.bak",
|
|
90
|
+
],
|
|
91
|
+
check=True,
|
|
92
|
+
)
|
|
93
|
+
break
|
|
94
|
+
elif choice == 3:
|
|
95
|
+
# Restore backup first
|
|
96
|
+
subprocess.run(
|
|
97
|
+
[
|
|
98
|
+
"sudo",
|
|
99
|
+
"cp",
|
|
100
|
+
"/etc/pam.d/sudo.bak",
|
|
101
|
+
"/etc/pam.d/sudo",
|
|
102
|
+
],
|
|
103
|
+
check=True,
|
|
104
|
+
)
|
|
105
|
+
typer.echo("✓ Original sudo PAM file has been restored")
|
|
106
|
+
break
|
|
107
|
+
else:
|
|
108
|
+
typer.echo("Invalid choice, please try again")
|
|
109
|
+
continue
|
|
110
|
+
else:
|
|
111
|
+
# Create a backup of the original file
|
|
112
|
+
subprocess.run(
|
|
113
|
+
["sudo", "cp", "/etc/pam.d/sudo", "/etc/pam.d/sudo.bak"],
|
|
114
|
+
check=True,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Add the line at the top of the sudo PAM file using a temporary file
|
|
118
|
+
subprocess.run(
|
|
119
|
+
[
|
|
120
|
+
"sudo",
|
|
121
|
+
"sh",
|
|
122
|
+
"-c",
|
|
123
|
+
'echo "auth sufficient pam_tid.so" | cat - /etc/pam.d/sudo > /tmp/sudo.pam && sudo mv /tmp/sudo.pam /etc/pam.d/sudo',
|
|
124
|
+
],
|
|
125
|
+
check=True,
|
|
126
|
+
)
|
|
127
|
+
typer.echo("✓ Touch ID for sudo has been enabled")
|
|
128
|
+
typer.echo("✓ Original file backed up as /etc/pam.d/sudo.bak")
|
|
129
|
+
|
|
130
|
+
except subprocess.CalledProcessError as e:
|
|
131
|
+
typer.echo(f"Error enabling Touch ID for sudo: {e}", err=True)
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@system_app.command()
|
|
136
|
+
def reset(
|
|
137
|
+
touchid: bool = typer.Option(
|
|
138
|
+
False, "-t", "--touchid", help="Reset Touch ID sudo configuration"
|
|
139
|
+
),
|
|
140
|
+
all: bool = typer.Option(
|
|
141
|
+
False, "-a", "--all", help="Reset all system settings to defaults"
|
|
142
|
+
),
|
|
143
|
+
):
|
|
144
|
+
"""Reset system settings. Must specify -t (touchid) or -a (all)."""
|
|
145
|
+
if not any([touchid, all]):
|
|
146
|
+
typer.echo("Error: Must specify either -t (touchid) or -a (all)")
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
if touchid or all:
|
|
151
|
+
# Show warning and get confirmation first
|
|
152
|
+
typer.echo("\n⚠️ This operation will:")
|
|
153
|
+
typer.echo(" 1. Remove Touch ID authentication from sudo")
|
|
154
|
+
typer.echo(" 2. Require sudo privileges to make these changes")
|
|
155
|
+
|
|
156
|
+
if not typer.confirm("\nDo you want to proceed?", default=False):
|
|
157
|
+
typer.echo("Operation cancelled")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Check if Touch ID is enabled
|
|
161
|
+
result = subprocess.run(
|
|
162
|
+
["grep", "auth sufficient pam_tid.so", "/etc/pam.d/sudo"],
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if result.returncode != 0:
|
|
168
|
+
typer.echo("Touch ID is not enabled in sudo configuration")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
while True:
|
|
172
|
+
typer.echo("\nPlease choose an option:")
|
|
173
|
+
typer.echo("0 - Do nothing and exit")
|
|
174
|
+
typer.echo("1 - View the backup file contents")
|
|
175
|
+
typer.echo("2 - Restore from stored backup")
|
|
176
|
+
|
|
177
|
+
choice = typer.prompt(
|
|
178
|
+
"\nEnter your choice (0-2)", type=int, default=0
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if choice == 0:
|
|
182
|
+
typer.echo("Operation cancelled")
|
|
183
|
+
return
|
|
184
|
+
elif choice == 1:
|
|
185
|
+
# Check if backup exists
|
|
186
|
+
result = subprocess.run(
|
|
187
|
+
["test", "-f", "/etc/pam.d/sudo.bak"],
|
|
188
|
+
capture_output=True,
|
|
189
|
+
text=True,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if result.returncode != 0:
|
|
193
|
+
typer.echo(
|
|
194
|
+
"No backup file found at /etc/pam.d/sudo.bak"
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
print_file_contents("/etc/pam.d/sudo.bak")
|
|
199
|
+
continue
|
|
200
|
+
elif choice == 2:
|
|
201
|
+
# Check if backup exists
|
|
202
|
+
result = subprocess.run(
|
|
203
|
+
["test", "-f", "/etc/pam.d/sudo.bak"],
|
|
204
|
+
capture_output=True,
|
|
205
|
+
text=True,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if result.returncode != 0:
|
|
209
|
+
typer.echo(
|
|
210
|
+
"No backup file found at /etc/pam.d/sudo.bak"
|
|
211
|
+
)
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# Restore the backup file
|
|
215
|
+
subprocess.run(
|
|
216
|
+
[
|
|
217
|
+
"sudo",
|
|
218
|
+
"cp",
|
|
219
|
+
"/etc/pam.d/sudo.bak",
|
|
220
|
+
"/etc/pam.d/sudo",
|
|
221
|
+
],
|
|
222
|
+
check=True,
|
|
223
|
+
)
|
|
224
|
+
typer.echo(
|
|
225
|
+
"✓ Touch ID sudo configuration has been reset from backup"
|
|
226
|
+
)
|
|
227
|
+
break
|
|
228
|
+
else:
|
|
229
|
+
typer.echo("Invalid choice, please try again")
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if all:
|
|
233
|
+
# Add more system settings here as they are implemented
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
except subprocess.CalledProcessError as e:
|
|
237
|
+
typer.echo(f"Error resetting system settings: {e}", err=True)
|
|
238
|
+
raise typer.Exit(1)
|