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.
@@ -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)