Track UI Events and Network Activity in macOS Using Rust + SwiftUI

Stephen CollinsStephen Collins
5 min read

Most macOS apps are black boxes. You click a button, something happens—maybe a network request, maybe some system interaction—but it's hard to know what unless you own the source code.

So I built a tool to watch.

This post walks through a hybrid SwiftUI + Rust project that logs every button click and tracks network traffic tied to the clicked app. It combines:

  • A SwiftUI frontend to simulate normal GUI interaction

  • A Rust backend using CGEventTap and the macOS Accessibility API

  • Per-process network inspection using the undocumented but powerful nettop

No kernel extensions. No root. Just clever usage of system APIs and a bit of FFI.

The full source code is available here.


Why build this?

Because reverse-engineering app behavior shouldn't require a debugger or Wireshark session. I wanted to:

  • Trace which UI elements trigger which network activity

  • Understand what apps do in response to input

  • Have a working reference for macOS Accessibility APIs in Rust

The Windows version of this tool relied on Win32 hooks and low-level TCP inspection via GetExtendedTcpTable. You can read that walkthrough here →

This macOS build takes the same spirit to Apple's ecosystem—same goals, different APIs.


Demo: From Click to Packet

When a user clicks Button A, here's what gets logged:

📡 example-mac-app.47727 ↑ 6092 B ↓ 0 B (Δ ↑ 6092 ↓ 0)
[INFO] Button Clicked: App='example-mac-app', PID=47727, ID='ButtonA', Label='Button A'

You get:

  • The app name

  • The PID

  • The element's accessibility label and identifier

  • A delta of bytes sent/received from that process (captured via nettop)


Architecture Overview

+----------------------+       +-----------------------+
|   SwiftUI Mac App    | --->  |   Rust macos-watcher  |
| (3 Buttons + Network)|       | (Event Tap + nettop)  |
+----------------------+       +-----------------------+
              ⬑ Accessibility API (AXUIElement)
                        ⬐ CGEventTap mouse/keyboard

Frontend: SwiftUI Playground

The Xcode app has a simple interface with three buttons (ButtonA, ButtonB, ButtonC). When clicked, each makes a network request using URLSession.

Button("Button A") {
    fetchProduct(id: 1)
}
.accessibilityIdentifier("ButtonA")

The important part is setting .accessibilityIdentifier() so we can extract metadata from the Rust process using the Accessibility API.


Backend: Rust Input + Network Logger

This is where the real work happens.

🔹 Event Hooking

We use CGEventTap to hook into global input events like left-clicks and key presses.

let event_mask = (1 << K_CG_EVENT_LEFT_MOUSE_DOWN) | (1 << K_CG_EVENT_KEY_DOWN);
let event_tap = CGEventTapCreate(..., event_mask, event_callback, ...);

🔹 Accessibility API

On every click, we use AXUIElementCopyElementAtPosition to resolve which UI element is under the cursor. We extract:

  • PID of the owning app

  • Accessibility identifier (if set)

  • Role (e.g. AXButton)

  • Optional description or label

let result = ax_ui_element_copy_element_at_position(
    system_wide_element, x, y, &mut element_ref);

Then:

let pid = ax_ui_element_get_pid(element_ref);
let role = ax_ui_element_copy_attribute_value(element_ref, K_AX_ROLE_ATTRIBUTE);

🔹 Network Activity via nettop

After identifying the UI element and its app PID, we shell out to:

nettop -P -J bytes_in,bytes_out -x -l 1

We parse the output for lines matching the PID (e.g. example-mac-app.47727), and log any change in byte counts. This gives us a primitive—but real—view into per-process network activity.


Log Output Example

📡 example-mac-app.47727 ↑ 6092 B ↓ 0 B (Δ ↑ 6092 ↓ 0)
[INFO] Button Clicked: App='example-mac-app', PID=47727, ID='ButtonA', Label='Button A'

Each input event is logged with as much context as we can scrape. You can see which button triggered the traffic, and how much was sent/received.


Installation & Permissions

To run the watcher, build the Rust CLI and grant it accessibility permissions:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cd macos-watcher
cargo build --release
./target/release/macos-watcher

macOS will prompt you to allow the binary under:

System Settings → Privacy & Security → Accessibility

Once granted, you'll see logs at:

~/macos_watcher.log

You can also run it persistently in the background using the included .plist and install_daemon.sh.


Dev Notes

  • Built using core-foundation, objc, and simplelog

  • All UI inspection is done using C-level bindings to AXUIElement* APIs

  • There's no NSAccessibility or AppKit dependency in the Rust half

  • SwiftUI code is sandboxed and declarative. No extra work needed to simulate real-world interaction


Use Cases

  • Security - See if random apps are sending packets when buttons are clicked

  • Testing - Ensure your app only hits the network when expected

  • Debugging - Tie UI behavior to system-level effects

  • Education - Learn how accessibility and input work on macOS


Limitations

  • Requires GUI permission to run (can't be run headless)

  • nettop output is undocumented and may change

  • Packet contents aren't inspected—this is about attribution, not introspection

  • Only tracks visible, clickable elements (not gestures, background jobs, etc.)


Cross-Platform Parity

We now have both Windows and macOS versions of this tool, each using the platform's native APIs:

FeatureWindowsmacOS
Input HookingWin32 Low-Level HooksCGEventTap
UI Element MetadataUIAutomation / IAccessibleAXUIElement
Network TrackingGetExtendedTcpTablenettop
LanguageRustRust + SwiftUI

Future Work

  • Add packet inspection (via libpcap) for HTTP tracing

  • Tag UI actions with timestamps to correlate more precisely with network

  • Export events in JSON or send to external observability tools


Closing Thoughts

This project taught me a lot about how macOS input and accessibility work under the hood. The fact that you can hook into system-wide mouse events, pull out UI metadata, and match it to network traffic—all with a user-space Rust binary—is kind of wild.

If you want to analyze how apps behave without reverse-engineering them from scratch, this tool gives you a solid start.

Code is available here.

0
Subscribe to my newsletter

Read articles from Stephen Collins directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Stephen Collins
Stephen Collins

Senior Software engineer currently working with a climate-tech startup