Skip to content

Commit 2859e39

Browse files
bqueninclaude
andauthored
Add macOS permissions UI section (#184)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1573a25 commit 2859e39

File tree

3 files changed

+214
-4
lines changed

3 files changed

+214
-4
lines changed

src/interpreter/gui/main_window.py

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
from ..capture.convert import bgra_to_rgb_pil
2626
from ..config import Config
2727
from ..overlay import BannerOverlay, InplaceOverlay
28+
from ..permissions import (
29+
check_accessibility,
30+
check_screen_recording,
31+
is_macos,
32+
open_accessibility_settings,
33+
open_screen_recording_settings,
34+
request_accessibility,
35+
request_screen_recording,
36+
)
2837
from . import keyboard
2938
from .workers import ProcessWorker
3039

@@ -140,6 +149,10 @@ def _setup_ui(self):
140149

141150
layout.addWidget(models_group)
142151

152+
# macOS Permissions Section (only shown on macOS)
153+
if is_macos():
154+
self._setup_permissions_ui(layout)
155+
143156
# Window Selection
144157
window_group = QGroupBox("Window Selection")
145158
window_layout = QHBoxLayout(window_group)
@@ -301,6 +314,82 @@ def _setup_ui(self):
301314
# Status bar
302315
self.statusBar().showMessage("Idle")
303316

317+
def _setup_permissions_ui(self, layout: QVBoxLayout):
318+
"""Set up macOS permissions section."""
319+
permissions_group = QGroupBox("macOS Permissions")
320+
permissions_layout = QGridLayout(permissions_group)
321+
322+
# Screen Recording row
323+
permissions_layout.addWidget(QLabel("Screen Recording"), 0, 0)
324+
self._screen_recording_status = QLabel()
325+
permissions_layout.addWidget(self._screen_recording_status, 0, 1)
326+
self._screen_recording_btn = QPushButton("Grant")
327+
self._screen_recording_btn.setFixedWidth(80)
328+
self._screen_recording_btn.clicked.connect(self._on_request_screen_recording)
329+
permissions_layout.addWidget(self._screen_recording_btn, 0, 2)
330+
331+
# Accessibility row (required for global hotkeys)
332+
permissions_layout.addWidget(QLabel("Accessibility"), 1, 0)
333+
self._accessibility_status = QLabel()
334+
permissions_layout.addWidget(self._accessibility_status, 1, 1)
335+
self._accessibility_btn = QPushButton("Grant")
336+
self._accessibility_btn.setFixedWidth(80)
337+
self._accessibility_btn.clicked.connect(self._on_request_accessibility)
338+
permissions_layout.addWidget(self._accessibility_btn, 1, 2)
339+
340+
# Set column stretch
341+
permissions_layout.setColumnStretch(0, 1)
342+
343+
layout.addWidget(permissions_group)
344+
345+
# Initial permission check
346+
self._update_permissions_status()
347+
348+
def _update_permissions_status(self):
349+
"""Update the permission status indicators."""
350+
if not is_macos():
351+
return
352+
353+
# Screen Recording
354+
if check_screen_recording():
355+
self._screen_recording_status.setText("✓ Granted")
356+
self._screen_recording_status.setStyleSheet("color: green;")
357+
self._screen_recording_btn.setVisible(False)
358+
else:
359+
self._screen_recording_status.setText("✗ Required")
360+
self._screen_recording_status.setStyleSheet("color: red;")
361+
self._screen_recording_btn.setVisible(True)
362+
363+
# Accessibility
364+
if check_accessibility():
365+
self._accessibility_status.setText("✓ Granted")
366+
self._accessibility_status.setStyleSheet("color: green;")
367+
self._accessibility_btn.setVisible(False)
368+
else:
369+
self._accessibility_status.setText("✗ Required")
370+
self._accessibility_status.setStyleSheet("color: red;")
371+
self._accessibility_btn.setVisible(True)
372+
373+
def _on_request_screen_recording(self):
374+
"""Handle Screen Recording grant button click."""
375+
# Try to request permission (triggers system dialog if first time)
376+
if not request_screen_recording():
377+
# Already denied, open System Settings
378+
open_screen_recording_settings()
379+
380+
# Update status after a short delay (permission may take a moment to register)
381+
QTimer.singleShot(500, self._update_permissions_status)
382+
383+
def _on_request_accessibility(self):
384+
"""Handle Accessibility grant button click."""
385+
# Try to request permission (triggers system dialog)
386+
if not request_accessibility():
387+
# Already denied, open System Settings
388+
open_accessibility_settings()
389+
390+
# Update status after a short delay
391+
QTimer.singleShot(500, self._update_permissions_status)
392+
304393
def _load_models(self):
305394
"""Start worker thread and load OCR/translation models."""
306395
self.statusBar().showMessage("Loading models...")
@@ -357,10 +446,7 @@ def _update_status_label(self, label: QLabel, status: str):
357446

358447
def _update_fix_button_visibility(self):
359448
"""Show/hide the Fix Models button based on model status."""
360-
has_error = (
361-
self._ocr_status_label.text() == "Error"
362-
or self._translation_status_label.text() == "Error"
363-
)
449+
has_error = self._ocr_status_label.text() == "Error" or self._translation_status_label.text() == "Error"
364450
self._fix_models_btn.setVisible(has_error)
365451

366452
def _on_fix_models(self):
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Platform-specific permission checking."""
2+
3+
import platform
4+
5+
6+
def is_macos() -> bool:
7+
"""Check if running on macOS."""
8+
return platform.system() == "Darwin"
9+
10+
11+
if platform.system() == "Darwin":
12+
from .macos import (
13+
check_accessibility,
14+
check_screen_recording,
15+
open_accessibility_settings,
16+
open_screen_recording_settings,
17+
request_accessibility,
18+
request_screen_recording,
19+
)
20+
else:
21+
# Stub functions for non-macOS platforms
22+
def check_screen_recording() -> bool:
23+
return True
24+
25+
def check_accessibility() -> bool:
26+
return True
27+
28+
def request_screen_recording() -> bool:
29+
return True
30+
31+
def request_accessibility() -> bool:
32+
return True
33+
34+
def open_screen_recording_settings() -> None:
35+
pass
36+
37+
def open_accessibility_settings() -> None:
38+
pass
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""macOS permission checking and requesting."""
2+
3+
import subprocess
4+
5+
6+
def check_screen_recording() -> bool:
7+
"""Check if Screen Recording permission is granted.
8+
9+
Returns:
10+
True if permission is granted, False otherwise.
11+
"""
12+
try:
13+
import Quartz.CoreGraphics as CG
14+
15+
return CG.CGPreflightScreenCaptureAccess()
16+
except Exception:
17+
return False
18+
19+
20+
def check_accessibility() -> bool:
21+
"""Check if Accessibility permission is granted.
22+
23+
This is required for global hotkeys (pynput uses Quartz event taps).
24+
25+
Returns:
26+
True if permission is granted, False otherwise.
27+
"""
28+
try:
29+
from ApplicationServices import AXIsProcessTrusted
30+
31+
return AXIsProcessTrusted()
32+
except Exception:
33+
return False
34+
35+
36+
def request_screen_recording() -> bool:
37+
"""Request Screen Recording permission.
38+
39+
This will trigger the system permission dialog if the user
40+
hasn't been prompted before. If already denied, it won't
41+
re-prompt - user must go to System Settings.
42+
43+
Returns:
44+
True if permission is granted, False otherwise.
45+
"""
46+
try:
47+
import Quartz.CoreGraphics as CG
48+
49+
return CG.CGRequestScreenCaptureAccess()
50+
except Exception:
51+
return False
52+
53+
54+
def request_accessibility() -> bool:
55+
"""Request Accessibility permission.
56+
57+
This will prompt the user to grant accessibility access.
58+
Required for global hotkeys.
59+
60+
Returns:
61+
True if permission is granted, False otherwise.
62+
"""
63+
try:
64+
from ApplicationServices import AXIsProcessTrustedWithOptions
65+
from Foundation import NSDictionary
66+
67+
# kAXTrustedCheckOptionPrompt = True will show the prompt
68+
options = NSDictionary.dictionaryWithObject_forKey_(True, "AXTrustedCheckOptionPrompt")
69+
return AXIsProcessTrustedWithOptions(options)
70+
except Exception:
71+
return False
72+
73+
74+
def open_screen_recording_settings() -> None:
75+
"""Open System Settings to the Screen Recording pane."""
76+
subprocess.run(
77+
["open", "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"], check=False
78+
)
79+
80+
81+
def open_accessibility_settings() -> None:
82+
"""Open System Settings to the Accessibility pane."""
83+
subprocess.run(
84+
["open", "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"],
85+
check=False,
86+
)

0 commit comments

Comments
 (0)