← Back to project
● Shipped P2 Size S Mac utility

Mac-Translator — Architecture

Component diagram, event flow from Force Click to HUD, permission model, failure modes.

Architecture

Sister docs: PRD (intent), Implementation (deep-dive), Notes (decision log).

System view

flowchart TB
    classDef input fill:#cce0e8,stroke:#1a1a1d,color:#1a1a1d,stroke-width:2px
    classDef app fill:#faedd6,stroke:#1a1a1d,color:#1a1a1d,stroke-width:2px
    classDef os fill:#e0d5ed,stroke:#1a1a1d,color:#1a1a1d,stroke-width:2px
    classDef ext fill:#f4d6db,stroke:#1a1a1d,color:#1a1a1d,stroke-width:2px

    subgraph User["👆 User input"]
        FC["Force Click
(Force Touch trackpad)"] Key["⌘⌥T chord
(any keyboard)"] end subgraph macOS["🍎 macOS event layer"] Tap["CGEventTap
leftMouseDragged
+ kCGMouseEventPressure"] Mon["NSEvent global monitor
(key events only)"] AX["Accessibility API
AXFocusedUIElement
AXSelectedText"] end FC --> Tap Key --> Mon subgraph App["📦 Mac-Translator.app (ad-hoc signed)"] Trig["Trigger dispatcher
(pressure ≥0.7 + movement filter)"] Sel["Selection fetcher
(AX bridge)"] Client["DeepL API client
(URLSession)"] HUD["NSPanel HUD
(borderless, floating)"] Status["NSStatusItem
menubar icon"] Guard["Startup permission check
(AX + Input Monitoring)"] end Tap --> Trig Mon --> Trig Trig --> Sel Sel --> AX AX --> Sel Sel --> Client Client --> HUD Status --> Trig subgraph Ext["☁️ External"] DeepL["DeepL Free API
api-free.deepl.com"] end Client -.HTTPS POST.-> DeepL DeepL -.JSON.-> Client Guard -.startup log.-> Status class FC,Key input class Tap,Mon,AX os class Trig,Sel,Client,HUD,Status,Guard app class DeepL ext

Event flow — Force Click trigger

[1] User Force Clicks on trackpad while text is selected


[2] macOS dispatches leftMouseDragged events with kCGMouseEventPressure values
    (pressure ramps 0.0 → 1.0 as user presses)


[3] CGEventTap callback fires for every dragged event:
    - Read pressure = CGEventGetDoubleValueField(event, kCGMouseEventPressure)
    - If pressure ≥ 0.7 AND cumulative movement < 5px since mouseDown:
        → classify as Force Click (not press-and-drag)
    - Else: pass through, no action


[4] Debounce: ignore subsequent pressure ≥0.7 for 800ms
    (one Force Click can span 50+ dragged events)


[5] Selection fetcher (AX bridge):
    - systemWideElement = AXUIElementCreateSystemWide()
    - AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute, &focused)
    - AXUIElementCopyAttributeValue(focused, kAXSelectedTextAttribute, &text)
    - If text nil OR len(text) == 0 → bail silently (no HUD)


[6] DeepL API client:
    - URLSession POST https://api-free.deepl.com/v2/translate
      Authorization: DeepL-Auth-Key <key>
      body: text=<selection>&target_lang=VI (or EN depending on detected source)
    - Timeout 3s, fail loud (HUD shows "translation failed")


[7] NSPanel HUD:
    - frame.origin = NSEvent.mouseLocation + (12, -8)
    - clamp inside screen bounds
    - panel.becomesKeyOnlyIfNeeded = true (doesn't steal focus)
    - panel.level = .floating
    - SwiftUI view: translation text, ≤280 chars or truncate "…"


[8] Local event monitor on HUD:
    - .keyDown (any key) → dismiss
    - special-case Escape → dismiss
    - On dismiss: panel.orderOut, monitor removed

Event flow — ⌘⌥T fallback

[1] NSEvent.addGlobalMonitorForEvents(matching: .keyDown) fires for every keydown


[2] Filter: modifierFlags == [.command, .option] AND keyCode == kVK_ANSI_T


[3] → Same flow from step [5] above (selection fetcher → DeepL → HUD)

Why two trigger paths: Force Click pressure events are NOT delivered to NSEvent global monitors. Keyboard events ARE. So Force Click needs CGEventTap, ⌘⌥T uses the simpler NSEvent monitor.

Permission flow (first launch)

[1] App launch


[2] Startup permission check (Guard):
    - AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true])
        → if false: System Settings opens to Privacy → Accessibility
        → log "AX = false" to console
    - IOHIDCheckAccess(kIOHIDRequestTypeListenEvent)
        → if denied: System Settings → Input Monitoring
        → log "InputMonitoring = denied"


[3] User toggles permissions ON in System Settings


[4] ⚠️ App must be RESTARTED — event watchers registered while AX was false
    stay dead silently after AX is granted


[5] On restart:
    - CGEventTap created (now AX=true, tap actually fires)
    - NSEvent global monitor created
    - HUD ready

Component responsibilities

ComponentOwnsDoesn’t own
CGEventTapForce Click pressure detection, movement filterSelection extraction, translation
NSEvent global monitor⌘⌥T keyboard chord detectionMouse events (Force Click)
Selection fetcher (AX bridge)Pulling selected text via Accessibility APITriggering, translation, display
DeepL API clientHTTPS round-trip, auto-detect source language, timeoutWhat text to translate, where to show result
NSPanel HUDBorderless floating window, cursor positioning, Escape/keypress dismissTranslation logic, gesture handling
NSStatusItem (menubar)On/off toggle, quit, settings entryTranslation flow
Startup permission guardDetecting AX + Input Monitoring grants, promptingRestart prompt (user must do manually)

Failure modes & recovery

FailureDetectRecoveryTime
AX permission revoked (after rebuild)Startup log “AX = false”tccutil reset Accessibility com.your.app → retoggle → restart app~30s
Input Monitoring revokedStartup log “InputMonitoring = denied”Retoggle in System Settings → restart~30s
Force Click not firingCGEventTap callback never reachedVerify Input Monitoring grant; check tap wasn’t disabled by timeout (re-enable)~10s
DeepL API timeoutURLSession 3s timeoutHUD shows “translation failed”; user retriesn/a
DeepL quota exceededHTTP 456 responseHUD shows “quota exceeded”; defer until next monthuntil reset
Selected text is nil (focus on non-AX-cooperative app)AX bridge returns emptySilent bail, no HUDn/a
HUD off-screen (cursor near edge)NSScreen clamp logicHUD position clamped to visibleFrameautomatic
App rebuild between grant and testPermission silently revoked, watchers deadDocument ritual: never rebuild between grant and testn/a

Why these choices

DecisionAlternative consideredWhy this won
CGEventTap over NSEvent global monitor for Force ClickNSEvent.addGlobalMonitorForEvents(matching: .pressure)NSEvent global monitor silently doesn’t fire for .pressure events. Cost 4 hours of “why isn’t my handler called” before discovering this
AX API over forcing ⌘CSimulated ⌘C + read NSPasteboardClobbers the user’s clipboard; intrusive; doesn’t work in protected apps
NSPanel over NSWindowNSWindowNSPanel is borderless, no Dock tile, doesn’t steal focus, perfect for HUDs
DeepL over Apple Translate frameworkApple Translate (offline)DeepL quality on VN↔EN materially better; offline fallback deferred
Cursor-anchored HUD over screen-centeredCentered modalCentered = “modal panel = visit an app”. Cursor-anchored = “translation lives where you’re reading”
Single .app over installerDMG with installerPersonal app, drop-in install is enough; no auto-update needed
Ad-hoc sign over Developer IDApple Developer Program ($99/y)Personal use, accepts permission rebuild cost in exchange
Force Click primary, ⌘⌥T fallback⌘⌥T onlyGesture is the product (see PRD §5); chord covers non-trackpad cases

See also

  • Implementation — code structure, CGEventTap details, AX bridge code
  • Notes — the 6 hours of macOS gotchas