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