How Neru is structured internally: layers, boundaries, data flow, and the rules that keep platform code isolated.
Neru is a keyboard-driven navigation tool written in Go with an Objective-C bridge on macOS. It runs as a daemon with a thin CLI client.
This document owns system shape and rationale. What actually works on each platform lives in CROSS_PLATFORM.md; how to build and test lives in DEVELOPMENT.md.
Related: Cross-Platform Guide · Development Guide · Agent Guide
- System Overview
- Runtime Shape
- Design Principles
- The "One Rule"
- Component Architecture
- Codebase Navigation Guide
- Data Flow
- Mode Handler Locking
- Coordinate Systems and Units
- Error Handling and Graceful Degradation
- Runtime Capability Reporting
- Platform Boundaries in the CLI Layer
- Application Identifier Terminology
- Technology Stack
- Performance Considerations
- Security Architecture
- References
Neru runs as a background daemon that listens for global hotkeys and keyboard events. When activated it offers several navigation modes:
- Hints — overlays unique character labels on clickable UI elements
- Grid — divides the screen into a coordinate-based grid
- Recursive grid — recursive cell navigation with center preview and backtracking
- Scroll — Vim-style scrolling at the cursor position
The architecture targets low latency and cross-platform extensibility while integrating deeply with native APIs. macOS is the reference implementation; current per-platform support is tracked in CROSS_PLATFORM.md.
Neru is a daemon plus a thin CLI. neru launch starts the daemon;
neru hints, neru action left_click, neru config reload and friends dial a
Unix domain socket ($TMPDIR/neru.sock, mode 0600) or a Windows named pipe —
see internal/adapter/ipc for the transport and
internal/app/ipcctrl for the command handlers.
New user-facing behavior therefore usually needs three pieces: a CLI command
(internal/cli/, registered in an init()), an IPC handler, and the
service/mode work behind it.
Startup is a numbered, individually-unwound phase sequence in new.go, with the individual steps in startup_phases.go:
1. infrastructure 4. UI components 7. IPC controller
2. services 4.5 systray 8. event tap + IPC server
3. application state 5. render components 9. shutdown channel
6. mode handler
Dependency injection is manual and explicit. Each phase that allocates
something appends a cleanup closure; on failure the app records failurePhase
and runs those closures in reverse (slices.Backward), so a half-built daemon
never lingers.
Neru follows a layered Hexagonal Architecture (Ports and Adapters):
- Shared business logic — hint generation, grid calculations, mode
transitions are pure Go in
internal/domainandinternal/app/services. - Platform isolation — OS-specific code is strictly quarantined.
- Ports and adapters — every system capability (Accessibility, Hotkeys,
Overlays) is an interface in
internal/ports, implemented by an adapter ininternal/adapter. - Build tag separation — OS-specific files carry build tags (
//go:build darwin) so they compile only for their target. - Platform roles over brand names — shared code says "primary modifier",
"display server", "accessibility backend", never
Cmdor a single display stack. - Build strategy follows backend choice — CGO is a per-backend-family decision, not a per-OS one. macOS requires it; Linux and Windows mix pure-Go and CGO-backed implementations by subsystem.
Where platform code physically goes, which file slot to use, and how the Linux backend family is organized are contributor concerns owned by CROSS_PLATFORM.md. The architectural source of truth for per-subsystem backend family, primary-modifier expectations, and build mode is profile.go.
Non-darwin-tagged code must never import
internal/adapter/platform/darwin.
Enforced twice: depguard in .golangci.yml, and
dependency_boundary_test.go.
The only exemptions are platform/darwin/**, *_darwin.go, and
*integration_darwin_test.go.
Cross the boundary through ports.SystemPort or a build-tagged dispatch pair
(platform_darwin.go / platform_other.go).
graph TD
subgraph "Presentation Layer"
CLI[internal/cli]
end
subgraph "Application Layer"
App[internal/app/app.go]
Modes[internal/app/modes]
Services[internal/app/services]
end
subgraph "Domain Layer"
Ports[internal/ports]
Domain[internal/domain]
end
subgraph "Adapters Layer"
Adapters[internal/adapter]
Platform[internal/adapter/platform]
end
CLI -->|IPC| App
App --> Services
Modes --> Services
Services --> Ports
Ports --> Domain
Adapters -.->|Implements| Ports
Platform -.->|Implements| Ports
- Domain (
internal/domain) — pure business logic and entities (hint.go, grid.go). No external dependencies. - Ports (
internal/ports) — interface contracts defining system capabilities (accessibility.go, overlay.go, font.go). - Application (
internal/app) — orchestrates domain entities and services; owns lifecycle and navigation modes. - Adapters (
internal/adapter) — concrete port implementations on platform APIs. - Overlay (
internal/adapter/overlay) — the adapter behindports.OverlayPort: it resolves styles, builds its own render components and owns the sequence a mode transition needs. A mode hands it a Frame; pure coordinate math lives ininternal/domain/geometry. - CLI (
internal/cli) — user commands, config loading, IPC to the daemon.
A directory-by-directory map for placing new code is in DEVELOPMENT.md.
The fastest way to understand Neru is to follow one event from the OS to the user-visible action.
1. Entry points
- main_darwin.go — bootstraps the app, locking the main thread for Cocoa
- root.go — the Cobra root command
2. Application wiring
- new.go — startup phases
- startup_phases.go — the individual infrastructure, service, and UI steps
3. The platform factory
factory.go and its build-tagged
siblings are the only place that picks a ports.SystemPort implementation. On
Linux there is a second, runtime axis on top of build tags:
backend_linux.go detects the
live compositor (wlroots / KDE / GNOME / other) and the factory routes to it.
4. Where a platform's code lives
Each OS capability is a package under internal/adapter/. Where a backend is a
real implementation rather than a few dispatch functions, it gets its own
directory and the directory names the platform:
adapter/eventtap/{tap,darwin,linux,windows} keyboard capture
adapter/hotkeys/{darwin,linux,windows} global hotkeys
adapter/systray/{darwin,linux,windows} tray icon
adapter/accessibility/{ax,atspi,native} element discovery
adapter/overlay/{manager,darwin,linux,windows} overlay rendering
adapter/platform/{darwin,linux,windows} the native cgo bridges
The parent package holds the port adapter and a small build-tagged factory —
the only place that knows which implementation exists. So "what do I touch to
add a compositor?" is answered by ls, not by reading build tags. When a
backend earns its own package and when build-tagged files in one package are
clearer is covered in
CROSS_PLATFORM.md.
5. Input processing
- OS — eventtap_darwin.m captures low-level keyboard events (Linux/Windows have equivalents)
- Adapters — adapter.go receives and dispatches them
- Application — handler.go routes the key to the active Mode
- Service — the mode calls into hint_service.go and friends
- Keyboard layout changes — on macOS the mode-level CGEventTap rebuilds its
key-name lookup tables at runtime (
NeruSetKeymapLayoutChangeCallbackin keymap_darwin.m) so navigation keys survive layout switches. Per-hotkey CGEventTaps re-register too (NeruSetKeymapLayoutChangeCallback2), becauseNeruKeyNameToCodemaps key names to layout-aware keycodes.
sequenceDiagram
participant OS as Operating System
participant ET as Event Tap (Infra)
participant H as Handler (App)
participant M as Active Mode (App)
participant S as Service (App)
participant A as Adapter (Infra)
OS->>ET: Key Down Event
ET->>H: Dispatch Key
H->>M: HandleKey(key)
M->>S: Process Logic
S->>A: Perform Action (e.g., Click)
A->>OS: Native API Call
sequenceDiagram
participant M as Mode (App)
participant S as Service (App)
participant OA as Overlay Adapter (Infra)
participant B as Bridge (CGo)
participant C as Cocoa (macOS)
M->>S: Request Display
S->>OA: ShowOverlay(elements)
OA->>B: DrawLabels(rects)
B->>C: Render Native Windows
On macOS each component owns its own NSPanel and calls the Objective-C bridge directly. On Linux and Windows the overlay manager does all drawing into one shared surface, and the per-component files are style-only stubs — see CROSS_PLATFORM.md.
Native macOS classes are wrapped in CGo so Go can call Cocoa while keeping type
safety. Location: internal/adapter/platform/darwin/; key files bridge.go,
overlay_darwin.m, accessibility_element_darwin.m.
modes.Handler is split so the compiler enforces its locking discipline, and
Mode.Activate / HandleKey / Exit all run with the lock already held. The
full contract — the Handler / handlerState split, the outer escape hatch
for deferred callbacks, and the moveMonitorMu → h.mu lock order — lives in
internal/app/modes/AGENTS.md. Read it before
touching modes or anything that calls back into the handler.
All shared code uses a global top-left (0,0) coordinate system.
- Origin — (0,0) is the top-left corner of the primary display
- Y-axis — increases downwards
- Units — screen pixels, unscaled
macOS Cocoa uses a bottom-left origin with Y increasing upwards. The inversion
happens inside the darwin adapter
(accessibility_screen_darwin.m)
— flipped coordinates must never leak into shared Go. Conversions live in
internal/domain/geometry.
Neru uses the custom derrors package:
derrors.New(code, msg) and derrors.Wrap(err, code, msg).
Unimplemented platform behavior must return CodeNotSupported explicitly rather
than silently no-oping:
return derrors.New(derrors.CodeNotSupported, "feature X not yet implemented on linux")Callers in the service layer degrade gracefully via derrors.IsNotSupported(err)
— typically logging a warning instead of surfacing an error. Prefer
CodeNotSupported over a silent no-op unless the operation is explicitly
documented as best-effort.
Adapters report a capability matrix stricter than "it compiles": supported
vs stub, surfaced to users by neru doctor. The registry
(capabilities.go,
capability_presets.go) must stay in
sync with reality; the policy and per-platform status live in
CROSS_PLATFORM.md.
neru services — the command itself is shared:
services.go registers ServicesCmd
unconditionally and delegates to unexported helpers (installService,
startService, …). The helpers are a Tier-2 dispatch pair:
services_darwin.go (//go:build darwin)
drives launchctl and .plist files, while
services_other.go (//go:build !darwin)
returns CodeNotSupported. Adding Linux service management means carving a
services_linux.go out of the !darwin slot and implementing the same helpers
over systemctl — registration is already shared, so no new init() is
needed.
IsRunningFromAppBundle — root.go delegates to
a build-tagged implementation: root_darwin.go
detects .app/Contents/MacOS paths so the daemon auto-starts when
double-clicked in Finder, root_windows.go
detects launches from Explorer / the Start Menu, and
root_other.go returns false.
Main-thread locking — on macOS
main_darwin.go calls runtime.LockOSThread()
before anything else, required by Cocoa. Non-macOS builds omit it. Never add
LockOSThread to shared code.
The codebase says "bundle ID" generically for the platform application identifier:
| Platform | Term | Example |
|---|---|---|
| macOS | Bundle ID | com.apple.Safari |
| Linux | Desktop ID / executable | firefox.desktop or firefox |
| Windows | AppUserModelID / executable | Microsoft.Edge or msedge.exe |
ports.AccessibilityPort.FocusedAppBundleID returns whatever the platform uses,
and general.excluded_apps in the config should use the same format for the
target platform.
- Core language — Go 1.26+
- Native integration — CGo + Objective-C (macOS)
- CLI framework — Cobra
- Configuration — TOML
- IPC — Unix domain sockets (Windows named pipes)
- Build system — Just
- CI/CD — GitHub Actions + Release Please
GitHub Actions runs lint, unit, and integration tests on every PR. Windows
binaries cross-compile with CGO_ENABLED=0; Linux builds need CGO_ENABLED=1
(X11/Wayland native backends) and must run on a Linux host, as macOS does for
its own.
- Event tap latency — the event tap callback stays extremely lean to avoid system-wide keyboard lag; heavy processing is deferred to goroutines.
- Bounded accessibility walks — querying accessibility APIs is expensive, so
traversal is bounded rather than exhaustive:
maxDepthon the macOS walk (ax.go), andatspiMaxDepth/atspiMaxNodeson the Linux AT-SPI walk (atspi/client.go). - Caching — a TTL/LRU cache for computed grid layouts (grid/cache.go) and a cache of C string pointers for overlay styles (style_cache.go) keep repeated activations off the hot path.
- Native rendering — GPU-accelerated CoreAnimation on macOS, Cairo on Linux, GDI on Windows.
- Secure input detection — Neru detects when Secure Input is enabled (e.g. a focused password field) and suspends the event tap, preventing unintended key logging.
- Permissions — Accessibility permission is required on macOS; Neru requests only the minimum needed for UI interaction.
- IPC security — the Unix domain socket is created with restricted file permissions (0600), so only the current user can talk to the daemon.
- CROSS_PLATFORM.md — per-platform support and contributor guide
- DEVELOPMENT.md — build, test, debug, add code
- CONFIGURATION.md — configuration reference
- macOS Accessibility API