Skip to content

Latest commit

 

History

History
461 lines (355 loc) · 17.7 KB

File metadata and controls

461 lines (355 loc) · 17.7 KB

Architecture

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


Table of Contents


System Overview

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.


Runtime Shape

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.


Design Principles

Neru follows a layered Hexagonal Architecture (Ports and Adapters):

  1. Shared business logic — hint generation, grid calculations, mode transitions are pure Go in internal/domain and internal/app/services.
  2. Platform isolation — OS-specific code is strictly quarantined.
  3. Ports and adapters — every system capability (Accessibility, Hotkeys, Overlays) is an interface in internal/ports, implemented by an adapter in internal/adapter.
  4. Build tag separation — OS-specific files carry build tags (//go:build darwin) so they compile only for their target.
  5. Platform roles over brand names — shared code says "primary modifier", "display server", "accessibility backend", never Cmd or a single display stack.
  6. 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.


The "One Rule"

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).


Component Architecture

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
Loading

Layer responsibilities

  • 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 behind ports.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 in internal/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.


Codebase Navigation Guide

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

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

  1. OSeventtap_darwin.m captures low-level keyboard events (Linux/Windows have equivalents)
  2. Adaptersadapter.go receives and dispatches them
  3. Applicationhandler.go routes the key to the active Mode
  4. Service — the mode calls into hint_service.go and friends
  5. Keyboard layout changes — on macOS the mode-level CGEventTap rebuilds its key-name lookup tables at runtime (NeruSetKeymapLayoutChangeCallback in keymap_darwin.m) so navigation keys survive layout switches. Per-hotkey CGEventTaps re-register too (NeruSetKeymapLayoutChangeCallback2), because NeruKeyNameToCode maps key names to layout-aware keycodes.

Data Flow

Input event propagation

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
Loading

Overlay rendering

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
Loading

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.

The CGo bridge (macOS)

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.


Mode Handler Locking

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 moveMonitorMuh.mu lock order — lives in internal/app/modes/AGENTS.md. Read it before touching modes or anything that calls back into the handler.


Coordinate Systems and Units

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.


Error Handling and Graceful Degradation

Neru uses the custom derrors package: derrors.New(code, msg) and derrors.Wrap(err, code, msg).

The CodeNotSupported policy

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.


Runtime Capability Reporting

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.


Platform Boundaries in the CLI Layer

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.

IsRunningFromAppBundleroot.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.


Application Identifier Terminology

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.


Technology Stack

  • Core languageGo 1.26+
  • Native integrationCGo + Objective-C (macOS)
  • CLI frameworkCobra
  • ConfigurationTOML
  • IPC — Unix domain sockets (Windows named pipes)
  • Build systemJust
  • 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.


Performance Considerations

  1. Event tap latency — the event tap callback stays extremely lean to avoid system-wide keyboard lag; heavy processing is deferred to goroutines.
  2. Bounded accessibility walks — querying accessibility APIs is expensive, so traversal is bounded rather than exhaustive: maxDepth on the macOS walk (ax.go), and atspiMaxDepth / atspiMaxNodes on the Linux AT-SPI walk (atspi/client.go).
  3. 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.
  4. Native rendering — GPU-accelerated CoreAnimation on macOS, Cairo on Linux, GDI on Windows.

Security Architecture

  1. 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.
  2. Permissions — Accessibility permission is required on macOS; Neru requests only the minimum needed for UI interaction.
  3. IPC security — the Unix domain socket is created with restricted file permissions (0600), so only the current user can talk to the daemon.

References