macbot-cli 0.1.0__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.
- macbot/__init__.py +3 -0
- macbot/cli.py +48 -0
- macbot/commands/__init__.py +1 -0
- macbot/commands/apps.py +187 -0
- macbot/commands/audio.py +252 -0
- macbot/commands/bluetooth.py +261 -0
- macbot/commands/brightness.py +104 -0
- macbot/commands/clipboard.py +75 -0
- macbot/commands/notify.py +50 -0
- macbot/commands/say.py +94 -0
- macbot/commands/screenshot.py +109 -0
- macbot/commands/wifi.py +222 -0
- macbot/commands/window.py +234 -0
- macbot/context.py +47 -0
- macbot_cli-0.1.0.dist-info/METADATA +210 -0
- macbot_cli-0.1.0.dist-info/RECORD +19 -0
- macbot_cli-0.1.0.dist-info/WHEEL +4 -0
- macbot_cli-0.1.0.dist-info/entry_points.txt +2 -0
- macbot_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Bluetooth control commands."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..context import pass_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def has_blueutil() -> bool:
|
|
11
|
+
"""Check if blueutil is installed."""
|
|
12
|
+
result = subprocess.run(["which", "blueutil"], capture_output=True)
|
|
13
|
+
return result.returncode == 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def bluetooth():
|
|
18
|
+
"""Bluetooth status and control."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@bluetooth.command("status")
|
|
23
|
+
@pass_context
|
|
24
|
+
def bluetooth_status(ctx):
|
|
25
|
+
"""Get Bluetooth status.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
macbot bluetooth status
|
|
29
|
+
"""
|
|
30
|
+
if not has_blueutil():
|
|
31
|
+
ctx.error("blueutil not installed. Run: brew install blueutil")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
# Get power state
|
|
35
|
+
power_result = subprocess.run(
|
|
36
|
+
["blueutil", "-p"],
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
check=True,
|
|
40
|
+
)
|
|
41
|
+
power_on = power_result.stdout.strip() == "1"
|
|
42
|
+
|
|
43
|
+
# Get discoverable state
|
|
44
|
+
disc_result = subprocess.run(
|
|
45
|
+
["blueutil", "-d"],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
check=True,
|
|
49
|
+
)
|
|
50
|
+
discoverable = disc_result.stdout.strip() == "1"
|
|
51
|
+
|
|
52
|
+
ctx.output({
|
|
53
|
+
"power": "on" if power_on else "off",
|
|
54
|
+
"discoverable": discoverable,
|
|
55
|
+
})
|
|
56
|
+
except subprocess.CalledProcessError as e:
|
|
57
|
+
ctx.error(f"Failed to get Bluetooth status: {e.stderr}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@bluetooth.command("on")
|
|
61
|
+
@pass_context
|
|
62
|
+
def bluetooth_on(ctx):
|
|
63
|
+
"""Turn Bluetooth on.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
macbot bluetooth on
|
|
67
|
+
"""
|
|
68
|
+
if not has_blueutil():
|
|
69
|
+
ctx.error("blueutil not installed. Run: brew install blueutil")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
subprocess.run(
|
|
73
|
+
["blueutil", "-p", "1"],
|
|
74
|
+
check=True,
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
)
|
|
78
|
+
ctx.output({"bluetooth": "on"})
|
|
79
|
+
except subprocess.CalledProcessError as e:
|
|
80
|
+
ctx.error(f"Failed to turn Bluetooth on: {e.stderr}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@bluetooth.command("off")
|
|
84
|
+
@pass_context
|
|
85
|
+
def bluetooth_off(ctx):
|
|
86
|
+
"""Turn Bluetooth off.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
macbot bluetooth off
|
|
90
|
+
"""
|
|
91
|
+
if not has_blueutil():
|
|
92
|
+
ctx.error("blueutil not installed. Run: brew install blueutil")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
subprocess.run(
|
|
96
|
+
["blueutil", "-p", "0"],
|
|
97
|
+
check=True,
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
)
|
|
101
|
+
ctx.output({"bluetooth": "off"})
|
|
102
|
+
except subprocess.CalledProcessError as e:
|
|
103
|
+
ctx.error(f"Failed to turn Bluetooth off: {e.stderr}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@bluetooth.command("devices")
|
|
107
|
+
@click.option("--paired", "-p", is_flag=True, help="Show only paired devices")
|
|
108
|
+
@click.option("--connected", "-c", is_flag=True, help="Show only connected devices")
|
|
109
|
+
@pass_context
|
|
110
|
+
def bluetooth_devices(ctx, paired: bool, connected: bool):
|
|
111
|
+
"""List Bluetooth devices.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
macbot bluetooth devices
|
|
115
|
+
macbot bluetooth devices --paired
|
|
116
|
+
macbot bluetooth devices --connected
|
|
117
|
+
"""
|
|
118
|
+
if not has_blueutil():
|
|
119
|
+
ctx.error("blueutil not installed. Run: brew install blueutil")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
if connected:
|
|
123
|
+
result = subprocess.run(
|
|
124
|
+
["blueutil", "--connected"],
|
|
125
|
+
capture_output=True,
|
|
126
|
+
text=True,
|
|
127
|
+
check=True,
|
|
128
|
+
)
|
|
129
|
+
elif paired:
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
["blueutil", "--paired"],
|
|
132
|
+
capture_output=True,
|
|
133
|
+
text=True,
|
|
134
|
+
check=True,
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
result = subprocess.run(
|
|
138
|
+
["blueutil", "--paired"],
|
|
139
|
+
capture_output=True,
|
|
140
|
+
text=True,
|
|
141
|
+
check=True,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
devices = []
|
|
145
|
+
for line in result.stdout.strip().split("\n"):
|
|
146
|
+
if line.strip():
|
|
147
|
+
# Parse: address: XX-XX-XX-XX-XX-XX, connected (paired, name: "Device Name")
|
|
148
|
+
parts = {}
|
|
149
|
+
if "address:" in line:
|
|
150
|
+
addr_match = line.split("address:")[1].split(",")[0].strip()
|
|
151
|
+
parts["address"] = addr_match
|
|
152
|
+
if "name:" in line:
|
|
153
|
+
# Extract name in quotes
|
|
154
|
+
import re
|
|
155
|
+
name_match = re.search(r'name:\s*"([^"]*)"', line)
|
|
156
|
+
if name_match:
|
|
157
|
+
parts["name"] = name_match.group(1)
|
|
158
|
+
parts["connected"] = "connected" in line.lower()
|
|
159
|
+
parts["paired"] = "paired" in line.lower()
|
|
160
|
+
if parts:
|
|
161
|
+
devices.append(parts)
|
|
162
|
+
|
|
163
|
+
ctx.output({"devices": devices, "count": len(devices)})
|
|
164
|
+
except subprocess.CalledProcessError as e:
|
|
165
|
+
ctx.error(f"Failed to list devices: {e.stderr}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@bluetooth.command("connect")
|
|
169
|
+
@click.argument("address")
|
|
170
|
+
@pass_context
|
|
171
|
+
def bluetooth_connect(ctx, address: str):
|
|
172
|
+
"""Connect to a Bluetooth device.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
macbot bluetooth connect XX-XX-XX-XX-XX-XX
|
|
176
|
+
"""
|
|
177
|
+
if not has_blueutil():
|
|
178
|
+
ctx.error("blueutil not installed. Run: brew install blueutil")
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
subprocess.run(
|
|
182
|
+
["blueutil", "--connect", address],
|
|
183
|
+
check=True,
|
|
184
|
+
capture_output=True,
|
|
185
|
+
text=True,
|
|
186
|
+
)
|
|
187
|
+
ctx.output({"address": address, "connected": True})
|
|
188
|
+
except subprocess.CalledProcessError as e:
|
|
189
|
+
ctx.error(f"Failed to connect: {e.stderr}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@bluetooth.command("disconnect")
|
|
193
|
+
@click.argument("address")
|
|
194
|
+
@pass_context
|
|
195
|
+
def bluetooth_disconnect(ctx, address: str):
|
|
196
|
+
"""Disconnect a Bluetooth device.
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
macbot bluetooth disconnect XX-XX-XX-XX-XX-XX
|
|
200
|
+
"""
|
|
201
|
+
if not has_blueutil():
|
|
202
|
+
ctx.error("blueutil not installed. Run: brew install blueutil")
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
subprocess.run(
|
|
206
|
+
["blueutil", "--disconnect", address],
|
|
207
|
+
check=True,
|
|
208
|
+
capture_output=True,
|
|
209
|
+
text=True,
|
|
210
|
+
)
|
|
211
|
+
ctx.output({"address": address, "disconnected": True})
|
|
212
|
+
except subprocess.CalledProcessError as e:
|
|
213
|
+
ctx.error(f"Failed to disconnect: {e.stderr}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@bluetooth.command("pair")
|
|
217
|
+
@click.argument("address")
|
|
218
|
+
@pass_context
|
|
219
|
+
def bluetooth_pair(ctx, address: str):
|
|
220
|
+
"""Pair with a Bluetooth device.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
macbot bluetooth pair XX-XX-XX-XX-XX-XX
|
|
224
|
+
"""
|
|
225
|
+
if not has_blueutil():
|
|
226
|
+
ctx.error("blueutil not installed. Run: brew install blueutil")
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
subprocess.run(
|
|
230
|
+
["blueutil", "--pair", address],
|
|
231
|
+
check=True,
|
|
232
|
+
capture_output=True,
|
|
233
|
+
text=True,
|
|
234
|
+
)
|
|
235
|
+
ctx.output({"address": address, "paired": True})
|
|
236
|
+
except subprocess.CalledProcessError as e:
|
|
237
|
+
ctx.error(f"Failed to pair: {e.stderr}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@bluetooth.command("unpair")
|
|
241
|
+
@click.argument("address")
|
|
242
|
+
@pass_context
|
|
243
|
+
def bluetooth_unpair(ctx, address: str):
|
|
244
|
+
"""Unpair a Bluetooth device.
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
macbot bluetooth unpair XX-XX-XX-XX-XX-XX
|
|
248
|
+
"""
|
|
249
|
+
if not has_blueutil():
|
|
250
|
+
ctx.error("blueutil not installed. Run: brew install blueutil")
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
subprocess.run(
|
|
254
|
+
["blueutil", "--unpair", address],
|
|
255
|
+
check=True,
|
|
256
|
+
capture_output=True,
|
|
257
|
+
text=True,
|
|
258
|
+
)
|
|
259
|
+
ctx.output({"address": address, "unpaired": True})
|
|
260
|
+
except subprocess.CalledProcessError as e:
|
|
261
|
+
ctx.error(f"Failed to unpair: {e.stderr}")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Display brightness control."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..context import pass_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.argument("level", required=False, type=float)
|
|
12
|
+
@click.option("--display", "-d", type=int, help="Display number (default: all)")
|
|
13
|
+
@pass_context
|
|
14
|
+
def brightness(ctx, level: float | None, display: int | None):
|
|
15
|
+
"""Get or set display brightness (0.0-1.0).
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
macbot brightness # Get current brightness
|
|
19
|
+
macbot brightness 0.7 # Set to 70%
|
|
20
|
+
macbot brightness 0.5 -d 1 # Set external display
|
|
21
|
+
"""
|
|
22
|
+
# Check for brightness command
|
|
23
|
+
try:
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
["which", "brightness"],
|
|
26
|
+
capture_output=True,
|
|
27
|
+
text=True,
|
|
28
|
+
)
|
|
29
|
+
has_brightness_cli = result.returncode == 0
|
|
30
|
+
except Exception:
|
|
31
|
+
has_brightness_cli = False
|
|
32
|
+
|
|
33
|
+
if level is not None:
|
|
34
|
+
# Set brightness
|
|
35
|
+
if level < 0 or level > 1:
|
|
36
|
+
ctx.error("Brightness must be between 0.0 and 1.0")
|
|
37
|
+
|
|
38
|
+
if has_brightness_cli:
|
|
39
|
+
cmd = ["brightness", str(level)]
|
|
40
|
+
if display:
|
|
41
|
+
cmd = ["brightness", "-d", str(display), str(level)]
|
|
42
|
+
try:
|
|
43
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
44
|
+
ctx.output({"brightness": level, "display": display, "set": True})
|
|
45
|
+
except subprocess.CalledProcessError as e:
|
|
46
|
+
ctx.error(f"Failed to set brightness: {e.stderr}")
|
|
47
|
+
else:
|
|
48
|
+
# Use AppleScript for built-in display
|
|
49
|
+
# Note: AppleScript brightness is 0-100
|
|
50
|
+
as_level = level * 100
|
|
51
|
+
script = f'''
|
|
52
|
+
tell application "System Preferences"
|
|
53
|
+
reveal anchor "displaysDisplayTab" of pane id "com.apple.preference.displays"
|
|
54
|
+
end tell
|
|
55
|
+
tell application "System Events"
|
|
56
|
+
tell process "System Preferences"
|
|
57
|
+
set value of slider 1 of group 1 of tab group 1 of window 1 to {as_level}
|
|
58
|
+
end tell
|
|
59
|
+
end tell
|
|
60
|
+
quit application "System Preferences"
|
|
61
|
+
'''
|
|
62
|
+
# Actually, this is unreliable. Use IOKit approach
|
|
63
|
+
ctx.error("brightness CLI not installed. Run: brew install brightness")
|
|
64
|
+
else:
|
|
65
|
+
# Get brightness
|
|
66
|
+
if has_brightness_cli:
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
["brightness", "-l"],
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
check=True,
|
|
73
|
+
)
|
|
74
|
+
# Parse output like "display 0: brightness 0.5"
|
|
75
|
+
displays = []
|
|
76
|
+
for line in result.stdout.strip().split("\n"):
|
|
77
|
+
if "brightness" in line:
|
|
78
|
+
parts = line.split("brightness")
|
|
79
|
+
if len(parts) >= 2:
|
|
80
|
+
try:
|
|
81
|
+
bright = float(parts[1].strip())
|
|
82
|
+
disp_num = 0
|
|
83
|
+
if "display" in parts[0]:
|
|
84
|
+
disp_num = int(parts[0].split("display")[1].strip().rstrip(":"))
|
|
85
|
+
displays.append({"display": disp_num, "brightness": bright})
|
|
86
|
+
except ValueError:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
if displays:
|
|
90
|
+
ctx.output({"displays": displays})
|
|
91
|
+
else:
|
|
92
|
+
ctx.error("Could not parse brightness output")
|
|
93
|
+
except subprocess.CalledProcessError as e:
|
|
94
|
+
ctx.error(f"Failed to get brightness: {e.stderr}")
|
|
95
|
+
else:
|
|
96
|
+
# Try AppleScript method for getting brightness
|
|
97
|
+
script = '''
|
|
98
|
+
tell application "System Events"
|
|
99
|
+
tell appearance preferences
|
|
100
|
+
return 0.5
|
|
101
|
+
end tell
|
|
102
|
+
end tell
|
|
103
|
+
'''
|
|
104
|
+
ctx.error("brightness CLI not installed. Run: brew install brightness")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Clipboard operations."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..context import pass_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def clipboard():
|
|
12
|
+
"""Clipboard get/set operations."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@clipboard.command("get")
|
|
17
|
+
@pass_context
|
|
18
|
+
def clipboard_get(ctx):
|
|
19
|
+
"""Get current clipboard contents.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
macbot clipboard get
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
["pbpaste"],
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
check=True,
|
|
30
|
+
)
|
|
31
|
+
content = result.stdout
|
|
32
|
+
ctx.output({"content": content, "length": len(content)})
|
|
33
|
+
except subprocess.CalledProcessError as e:
|
|
34
|
+
ctx.error(f"Failed to read clipboard: {e.stderr}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@clipboard.command("set")
|
|
38
|
+
@click.argument("text")
|
|
39
|
+
@pass_context
|
|
40
|
+
def clipboard_set(ctx, text: str):
|
|
41
|
+
"""Set clipboard contents.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
macbot clipboard set "Hello, World!"
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
subprocess.run(
|
|
48
|
+
["pbcopy"],
|
|
49
|
+
input=text,
|
|
50
|
+
text=True,
|
|
51
|
+
check=True,
|
|
52
|
+
)
|
|
53
|
+
ctx.output({"content": text, "length": len(text), "set": True})
|
|
54
|
+
except subprocess.CalledProcessError as e:
|
|
55
|
+
ctx.error(f"Failed to set clipboard: {e.stderr}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@clipboard.command("clear")
|
|
59
|
+
@pass_context
|
|
60
|
+
def clipboard_clear(ctx):
|
|
61
|
+
"""Clear clipboard contents.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
macbot clipboard clear
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
subprocess.run(
|
|
68
|
+
["pbcopy"],
|
|
69
|
+
input="",
|
|
70
|
+
text=True,
|
|
71
|
+
check=True,
|
|
72
|
+
)
|
|
73
|
+
ctx.output({"cleared": True})
|
|
74
|
+
except subprocess.CalledProcessError as e:
|
|
75
|
+
ctx.error(f"Failed to clear clipboard: {e.stderr}")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Send macOS notifications."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..context import pass_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.argument("title")
|
|
12
|
+
@click.argument("message", required=False, default="")
|
|
13
|
+
@click.option("--subtitle", "-s", help="Notification subtitle")
|
|
14
|
+
@click.option("--sound", "-S", help="Sound name (e.g., Ping, Basso, Glass)")
|
|
15
|
+
@pass_context
|
|
16
|
+
def notify(ctx, title: str, message: str, subtitle: str | None, sound: str | None):
|
|
17
|
+
"""Send a macOS notification.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
macbot notify "Build Complete" "All tests passed"
|
|
21
|
+
macbot notify "Alert" --subtitle "Warning" --sound Ping
|
|
22
|
+
"""
|
|
23
|
+
# Build AppleScript
|
|
24
|
+
script_parts = [f'display notification "{message}"']
|
|
25
|
+
script_parts.append(f'with title "{title}"')
|
|
26
|
+
|
|
27
|
+
if subtitle:
|
|
28
|
+
script_parts.append(f'subtitle "{subtitle}"')
|
|
29
|
+
|
|
30
|
+
if sound:
|
|
31
|
+
script_parts.append(f'sound name "{sound}"')
|
|
32
|
+
|
|
33
|
+
script = " ".join(script_parts)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
subprocess.run(
|
|
37
|
+
["osascript", "-e", script],
|
|
38
|
+
check=True,
|
|
39
|
+
capture_output=True,
|
|
40
|
+
text=True,
|
|
41
|
+
)
|
|
42
|
+
ctx.output({
|
|
43
|
+
"title": title,
|
|
44
|
+
"message": message,
|
|
45
|
+
"subtitle": subtitle,
|
|
46
|
+
"sound": sound,
|
|
47
|
+
"sent": True,
|
|
48
|
+
})
|
|
49
|
+
except subprocess.CalledProcessError as e:
|
|
50
|
+
ctx.error(f"Failed to send notification: {e.stderr}")
|
macbot/commands/say.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Text-to-speech command."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..context import pass_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.argument("text")
|
|
12
|
+
@click.option("--voice", "-v", help="Voice name (e.g., Alex, Samantha, Daniel)")
|
|
13
|
+
@click.option("--rate", "-r", type=int, help="Speech rate (words per minute)")
|
|
14
|
+
@click.option("--output", "-o", help="Save to audio file instead of speaking")
|
|
15
|
+
@click.option("--list-voices", is_flag=True, help="List available voices")
|
|
16
|
+
@pass_context
|
|
17
|
+
def say(
|
|
18
|
+
ctx,
|
|
19
|
+
text: str,
|
|
20
|
+
voice: str | None,
|
|
21
|
+
rate: int | None,
|
|
22
|
+
output: str | None,
|
|
23
|
+
list_voices: bool,
|
|
24
|
+
):
|
|
25
|
+
"""Speak text using macOS text-to-speech.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
macbot say "Hello, World!"
|
|
29
|
+
macbot say "British accent" --voice Daniel
|
|
30
|
+
macbot say "Fast speech" --rate 300
|
|
31
|
+
macbot say "Save this" --output speech.aiff
|
|
32
|
+
macbot say "" --list-voices
|
|
33
|
+
"""
|
|
34
|
+
if list_voices:
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["say", "-v", "?"],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=True,
|
|
41
|
+
)
|
|
42
|
+
voices = []
|
|
43
|
+
for line in result.stdout.strip().split("\n"):
|
|
44
|
+
if line.strip():
|
|
45
|
+
# Parse: "Voice Name lang # description"
|
|
46
|
+
parts = line.split()
|
|
47
|
+
if len(parts) >= 2:
|
|
48
|
+
# Voice name may have spaces, lang is before #
|
|
49
|
+
name_parts = []
|
|
50
|
+
lang = None
|
|
51
|
+
for i, part in enumerate(parts):
|
|
52
|
+
if part.startswith("#"):
|
|
53
|
+
break
|
|
54
|
+
if "_" in part or len(part) == 5: # Language codes like en_US
|
|
55
|
+
lang = part
|
|
56
|
+
break
|
|
57
|
+
name_parts.append(part)
|
|
58
|
+
voices.append({
|
|
59
|
+
"name": " ".join(name_parts),
|
|
60
|
+
"language": lang,
|
|
61
|
+
})
|
|
62
|
+
ctx.output({"voices": voices, "count": len(voices)})
|
|
63
|
+
return
|
|
64
|
+
except subprocess.CalledProcessError as e:
|
|
65
|
+
ctx.error(f"Failed to list voices: {e.stderr}")
|
|
66
|
+
|
|
67
|
+
cmd = ["say"]
|
|
68
|
+
|
|
69
|
+
if voice:
|
|
70
|
+
cmd.extend(["-v", voice])
|
|
71
|
+
|
|
72
|
+
if rate:
|
|
73
|
+
cmd.extend(["-r", str(rate)])
|
|
74
|
+
|
|
75
|
+
if output:
|
|
76
|
+
cmd.extend(["-o", output])
|
|
77
|
+
|
|
78
|
+
cmd.append(text)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
82
|
+
result = {
|
|
83
|
+
"text": text,
|
|
84
|
+
"voice": voice,
|
|
85
|
+
"rate": rate,
|
|
86
|
+
"spoken": True,
|
|
87
|
+
}
|
|
88
|
+
if output:
|
|
89
|
+
result["output"] = output
|
|
90
|
+
result["spoken"] = False
|
|
91
|
+
result["saved"] = True
|
|
92
|
+
ctx.output(result)
|
|
93
|
+
except subprocess.CalledProcessError as e:
|
|
94
|
+
ctx.error(f"Failed to speak: {e.stderr}")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Screenshot commands."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ..context import pass_context
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.argument("output", required=False)
|
|
14
|
+
@click.option("--clipboard", "-c", is_flag=True, help="Copy to clipboard instead of file")
|
|
15
|
+
@click.option("--window", "-w", is_flag=True, help="Capture front window only")
|
|
16
|
+
@click.option("--interactive", "-i", is_flag=True, help="Interactive selection mode")
|
|
17
|
+
@click.option("--screen", "-s", type=int, help="Capture specific screen (1, 2, ...)")
|
|
18
|
+
@click.option("--delay", "-d", type=int, help="Delay in seconds before capture")
|
|
19
|
+
@click.option("--no-shadow", is_flag=True, help="Disable window shadow")
|
|
20
|
+
@pass_context
|
|
21
|
+
def screenshot(
|
|
22
|
+
ctx,
|
|
23
|
+
output: str | None,
|
|
24
|
+
clipboard: bool,
|
|
25
|
+
window: bool,
|
|
26
|
+
interactive: bool,
|
|
27
|
+
screen: int | None,
|
|
28
|
+
delay: int | None,
|
|
29
|
+
no_shadow: bool,
|
|
30
|
+
):
|
|
31
|
+
"""Take a screenshot.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
macbot screenshot # Save to desktop with timestamp
|
|
35
|
+
macbot screenshot screen.png # Save to specific file
|
|
36
|
+
macbot screenshot --clipboard # Copy to clipboard
|
|
37
|
+
macbot screenshot --window # Capture front window
|
|
38
|
+
macbot screenshot --interactive # Select region
|
|
39
|
+
macbot screenshot --delay 3 # 3 second delay
|
|
40
|
+
"""
|
|
41
|
+
cmd = ["screencapture"]
|
|
42
|
+
|
|
43
|
+
# Silent mode (no camera sound)
|
|
44
|
+
cmd.append("-x")
|
|
45
|
+
|
|
46
|
+
if clipboard:
|
|
47
|
+
cmd.append("-c")
|
|
48
|
+
else:
|
|
49
|
+
if output is None:
|
|
50
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
51
|
+
desktop = os.path.expanduser("~/Desktop")
|
|
52
|
+
output = os.path.join(desktop, f"screenshot_{timestamp}.png")
|
|
53
|
+
|
|
54
|
+
if window:
|
|
55
|
+
cmd.append("-l")
|
|
56
|
+
# Get front window ID
|
|
57
|
+
script = '''
|
|
58
|
+
tell application "System Events"
|
|
59
|
+
set frontApp to first application process whose frontmost is true
|
|
60
|
+
set frontWindow to front window of frontApp
|
|
61
|
+
return id of frontWindow
|
|
62
|
+
end tell
|
|
63
|
+
'''
|
|
64
|
+
try:
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
["osascript", "-e", script],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
)
|
|
70
|
+
window_id = result.stdout.strip()
|
|
71
|
+
if window_id:
|
|
72
|
+
cmd.append(window_id)
|
|
73
|
+
except Exception:
|
|
74
|
+
# Fall back to interactive window selection
|
|
75
|
+
cmd = ["screencapture", "-x", "-w"]
|
|
76
|
+
|
|
77
|
+
if interactive:
|
|
78
|
+
cmd.append("-i")
|
|
79
|
+
|
|
80
|
+
if screen:
|
|
81
|
+
cmd.extend(["-D", str(screen)])
|
|
82
|
+
|
|
83
|
+
if delay:
|
|
84
|
+
cmd.extend(["-T", str(delay)])
|
|
85
|
+
|
|
86
|
+
if no_shadow:
|
|
87
|
+
cmd.append("-o")
|
|
88
|
+
|
|
89
|
+
if not clipboard and output:
|
|
90
|
+
cmd.append(output)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
94
|
+
|
|
95
|
+
result = {
|
|
96
|
+
"captured": True,
|
|
97
|
+
"clipboard": clipboard,
|
|
98
|
+
"window": window,
|
|
99
|
+
"interactive": interactive,
|
|
100
|
+
}
|
|
101
|
+
if output and not clipboard:
|
|
102
|
+
result["path"] = output
|
|
103
|
+
result["exists"] = os.path.exists(output)
|
|
104
|
+
if result["exists"]:
|
|
105
|
+
result["size"] = os.path.getsize(output)
|
|
106
|
+
|
|
107
|
+
ctx.output(result)
|
|
108
|
+
except subprocess.CalledProcessError as e:
|
|
109
|
+
ctx.error(f"Failed to capture screenshot: {e.stderr}")
|