Skip to content

Latest commit

 

History

History
969 lines (683 loc) · 28.8 KB

File metadata and controls

969 lines (683 loc) · 28.8 KB

Development Guide

Contributing to Neru: build instructions, architecture overview, and contribution guidelines.


Table of Contents


Quick Start

Get Neru running locally in 5 minutes:

# 1. Clone and setup
git clone https://github.com/y3owk1n/neru.git
cd neru

# 2. Set up development environment

## Option A: Using Devbox (Recommended)
devbox shell

See [Development Environment Options](#development-environment-options) for install details

## Option B: Manual installation
brew install just golangci-lint

See [Development Environment Options](#development-environment-options) for install details

# 3. Build and run
just build
./bin/neru launch

# 4. Test it works
neru hints  # Should show hint overlays

Need help? See Installation Guide for detailed setup instructions.


Development Setup

Prerequisites

  • Go 1.26+ - Install Go

  • Xcode Command Line Tools - xcode-select --install

  • Just - Command runner (Install)

    brew install just
  • golangci-lint - Linter (Install)

    brew install golangci-lint

Development Environment Options

For the best development experience, choose one of the following setup methods:

Option A: Devbox (Recommended)

Devbox provides an isolated development environment with all required tools pre-configured.

# Install Devbox
curl -fsSL https://get.jetify.com/devbox | bash

# Option 1: Enter the development shell manually
devbox shell

# Option 2: Use direnv for automatic shell activation (recommended)
# Install direnv: brew install direnv
# Add to your shell: eval "$(direnv hook bash)" (or zsh/fish)
# The .envrc file will automatically activate devbox when you cd into the project

Devbox automatically installs and manages:

  • Go 1.26+
  • gopls (Go language server)
  • gotools, gofumpt, golines (Go formatting tools)
  • golangci-lint (linter)
  • just (command runner)
  • clang-tools (C/C++ tools for CGo)

Option B: Manual Installation

Install essential tools manually using Homebrew. Note that Devbox provides additional development tools (gopls, gofumpt, golines, etc.) that can be installed separately if desired.

brew install go just golangci-lint llvm

Tool descriptions:

  • go - Go compiler and toolchain (1.26+ required)
  • just - Command runner for build scripts
  • golangci-lint - Go linter and formatter
  • llvm - LLVM tools including clang-format for C/C++/Objective-C formatting (required for CGo code)

Optional additional tools (install via go install if desired):

  • gopls: go install golang.org/x/tools/gopls@latest
  • gofumpt: go install mvdan.cc/gofumpt@latest
  • golines: go install github.com/segmentio/golines@latest

Clone Repository

git clone https://github.com/y3owk1n/neru.git
cd neru

Verify Setup

# Check Go version
go version  # Should be 1.26+

# Check tools
just --version
golangci-lint --version

# List available commands
just --list

Development Environment

For the best development experience, we recommend:

  1. IDE Setup: Use VS Code with Go extension or GoLand
  2. EditorConfig: Install EditorConfig plugin for consistent formatting
  3. Pre-commit Hooks: Set up git hooks to automate formatting and linting
# Install pre-commit hooks
cp scripts/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Common Development Tasks

Task Command Description
Build just build Compile the application
Build just build-darwin Build a macOS binary on macOS
Build just build-linux Build a Linux foundations binary
Build just build-windows Build a Windows foundations binary
Test just test Run unit and integration tests
Test just test-foundation Run cross-platform-safe core tests
Test just test-unit Run unit tests
Test just test-integration Run integration tests
Test just test-race Run all tests with race detection
Test just test-all Run all tests (unit + integration)
Lint just lint Run linters
Format just fmt Format code
Run just run Build and run the application
Clean just clean Remove build artifacts

Debugging

To debug Neru during development:

  1. Enable Debug Logging:

    [logging]
    log_level = "debug"
  2. View Logs:

    # macOS
    tail -f ~/Library/Logs/neru/app.log
    
    # Linux
    tail -f ~/.local/state/neru/log/app.log
  3. Use Delve Debugger:

    dlv debug ./cmd/neru

Building

Neru uses Just as a command runner (alternative to Make).

Quick Start

# Development build
just build

# Run
./bin/neru launch

Build Commands

# Development build (auto-detects version from git tags)
just build

# Build target-specific contributor binaries
just build-darwin
just build-linux
just build-windows

# Release build (optimized, stripped)
just release

# Build app bundle (macOS .app)
just bundle

# Build with custom version
just build-version v1.0.0

# Clean build artifacts
just clean

Cross-Platform Contributor Baseline

If you are starting Linux or Windows work, this is the recommended minimum smoke-test sequence:

just build
just test-foundation

Then, depending on what you are targeting:

  • Linux: just build-linux
  • Windows: just build-windows
  • macOS: just build-darwin

Testing Tiers

Neru uses a few different testing layers:

  1. Pure unit tests: Shared Go logic with no native OS dependency.
  2. Contract tests: Ports and adapters should agree on error semantics such as CodeNotSupported.
  3. Integration tests: Real OS/native behavior behind integration build tags.
  4. Architecture tests: Guardrails that protect package boundaries and platform isolation.

When you add a stubbed platform feature, add or update a contract test so the unsupported behavior is explicit and stable until the real implementation lands.

For cross-platform work, prefer shared terms over macOS-specific names:

  • Primary means the default accelerator modifier: Cmd on macOS, Ctrl on Linux/Windows.
  • Linux is not a single backend. Treat X11 and Wayland as separate infra concerns behind the same port.
  • Start backend decisions from internal/core/infra/platform/profile.go so contributors extend one source of truth.
  • Do not assume CGO based on OS alone. Check the backend plan first: some Linux and most early Windows integrations can stay pure Go, while macOS and some future Linux backends require CGO.

For practical file-by-file contributor guidance, see CROSS_PLATFORM.md.

Manual Build

Without Just:

# Basic build
go build -o bin/neru ./cmd/neru

# With version info
VERSION=$(git describe --tags --always --dirty)
go build \
  -ldflags="-s -w -X github.com/y3owk1n/neru/internal/cli.Version=$VERSION" \
  -o bin/neru \
  ./cmd/neru

# For release (optimized)
go build \
  -ldflags="-s -w -X github.com/y3owk1n/neru/internal/cli.Version=$VERSION" \
  -trimpath \
  -o bin/neru \
  ./cmd/neru

Build Flags Explained

  • -ldflags="-s -w" - Strip debug info and symbol table (smaller binary)
  • -trimpath - Remove file system paths from binary
  • -X pkg.Var=value - Set string variable at build time (version injection)

Testing

Neru has a comprehensive test suite with clear separation between unit tests and integration tests. For detailed testing guidelines and standards, see CODING_STANDARDS.md.

Test Organization

Test Type File Pattern Purpose Command Coverage
Unit Tests *_test.go Business logic with mocks (no build tag required) just test 50+ tests covering algorithms, isolated components
Integration Tests *_integration_*_test.go Real system interactions (tagged //go:build integration && <os>) just test-integration Tests covering platform APIs, IPC, file operations

Test File Naming Convention

package_test.go                          # Unit tests (logic, mocks)
package_integration_darwin_test.go       # macOS integration tests //go:build integration && darwin
package_integration_linux_test.go        # Linux integration tests  //go:build integration && linux

Run Tests

# Unit tests only (fast, CI)
just test

# Integration tests only (comprehensive, local)
just test-integration

# All tests (unit + integration)
just test-all

# With race detection
just test-race

Test Coverage Areas

Unit Test Coverage

  • Domain Logic: Hint generation, grid calculations, element filtering
  • Service Logic: Action processing, mode transitions, configuration validation
  • Adapter Interfaces: Port implementations with mocked dependencies
  • Configuration: TOML parsing, validation, defaults
  • CLI Logic: Command parsing, argument validation
  • Pure Logic Benchmarks: Performance testing of algorithms without system calls

Integration Test Coverage

  • macOS Accessibility API: Real UI element access, cursor control, mouse actions
  • macOS Event Tap API: Real global keyboard event interception
  • macOS Hotkey API: Real global hotkey registration/unregistration
  • Unix Socket IPC: Real inter-process communication
  • macOS Overlay API: Real window/overlay management
  • File System Operations: Real config file loading/reloading
  • Component Coordination: Real service-to-adapter interactions

Run Linter

# Run all linters
just lint

# Auto-fix issues
golangci-lint run --fix

Test During Development

# Watch mode (requires entr or similar)
find . -name "*.go" | entr -r just test

# Quick iteration with unit tests
just build && just test

# Full validation before commit
just test && just lint && just build

# Test specific package
go test ./internal/core/domain/hint/

# Test with verbose output
go test -v ./internal/app/services/

# Integration test specific component
go test -tags=integration ./internal/core/infra/accessibility/

Architecture Overview

What Neru Does

Neru is a keyboard-driven navigation tool that enhances productivity by allowing users to quickly navigate and interact with UI elements using keyboard shortcuts. macOS is the primary supported platform; Linux and Windows support is in progress (see ARCHITECTURE.md).

Four Navigation Modes:

  • Hints Mode: Uses macOS Accessibility APIs to identify clickable elements and overlay hint labels
  • Grid Mode: Divides the screen into a grid system for coordinate-based navigation
  • Scroll Mode: Provides Vim-style scrolling at the cursor position
  • Recursive Grid Mode: Recursive cell navigation with center preview and reset/backtrack support

All modes support various actions and can be configured extensively through TOML configuration files.

Mode Interface Contract

Neru uses a standardized Mode interface to ensure consistent behavior across all navigation modes. This interface defines the contract that all mode implementations must follow.

Interface Definition

type Mode interface {
    // Activate initializes and starts the mode with optional action parameters
    Activate(action *string)

    // HandleKey processes keyboard input during normal mode operation
    HandleKey(key string)

    // HandleActionKey processes keyboard input when in action sub-mode
    HandleActionKey(key string)

    // Exit performs cleanup and deactivates the mode
    Exit()

    // ToggleActionMode switches between normal mode and action sub-mode
    ToggleActionMode()

    // ModeType returns the domain mode type identifier
    ModeType() domain.Mode
}

Implementation Pattern

All mode implementations follow this pattern:

  1. Struct Definition: Create a struct that holds a reference to the Handler
  2. Constructor: Provide a NewXXXMode(handler *Handler) constructor
  3. Interface Methods: Implement all required interface methods
  4. Registration: Register the mode in Handler.NewHandler()

Method Contracts

Activate(action *string)
  • Purpose: Initialize the mode and set it as the active mode
  • Parameters: Optional action string for pending actions
  • Responsibilities:
    • Call handler.setModeLocked() to change app state (caller holds h.mu)
    • Show mode-specific overlays/UI
    • Initialize mode-specific state
    • Log mode activation
HandleKey(key string)
  • Purpose: Process keyboard input during normal mode operation
  • Parameters: Single key string (e.g., "a", "j", "escape")
  • Responsibilities:
    • Route keys to appropriate handlers
    • Update mode state based on input
    • Handle mode-specific navigation logic
HandleActionKey(key string)
  • Purpose: Process keyboard input when in action sub-mode
  • Parameters: Single key string representing action selection
  • Responsibilities:
    • Delegate to handler.handleActionKey() for action execution
    • Handle action-specific key mappings
Exit()
  • Purpose: Clean up mode state and return to idle
  • Responsibilities:
    • Hide overlays and UI elements
    • Reset mode-specific state
    • Perform mode-specific cleanup only (common cleanup is handled by exitModeLocked)
ToggleActionMode()
  • Purpose: Switch between normal mode and action sub-mode
  • Responsibilities:
    • Delegate to handler's toggle method (e.g., toggleActionModeForHints())
    • Handle mode-specific action mode transitions
ModeType()
  • Purpose: Return the domain mode identifier
  • Returns: domain.Mode enum value (e.g., domain.ModeHints)

Implementation Examples

Basic Mode Structure
type ExampleMode struct {
    handler *Handler
}

func NewExampleMode(handler *Handler) *ExampleMode {
    return &ExampleMode{handler: handler}
}

func (m *ExampleMode) ModeType() domain.Mode {
    return domain.ModeExample
}

func (m *ExampleMode) Activate(action *string) {
    // NOTE: Activate is called with h.mu already held (via ActivateModeWithAction).
    // Use the *Locked helpers — never the public SetMode* methods (deadlock).
    m.handler.setModeLocked(domain.ModeExample, overlay.ModeExample)
    // Show example overlay
    // Initialize state
    m.handler.logger.Info("Example mode activated")
}

func (m *ExampleMode) HandleKey(key string) {
    // NOTE: HandleKey is called with h.mu already held (via HandleKeyPress).
    // Use exitModeLocked — never the public SetModeIdle (deadlock).
    switch key {
    case "escape":
        m.handler.exitModeLocked()
    // Handle other keys...
    }
}

func (m *ExampleMode) HandleActionKey(key string) {
    m.handler.handleActionKey(key, "Example")
}

func (m *ExampleMode) Exit() {
    // Hide overlays
    // Reset state
}

func (m *ExampleMode) ToggleActionMode() {
    m.handler.toggleActionModeForExample()
}
Registration Pattern
func NewHandler(...) *Handler {
    handler := &Handler{...}
    handler.modes = map[domain.Mode]Mode{
        domain.ModeHints:  NewHintsMode(handler),
        domain.ModeGrid:   NewGridMode(handler),
        domain.ModeAction: NewActionMode(handler),
        domain.ModeScroll: NewScrollMode(handler),
        // Add new modes here
    }
    return handler
}

Best Practices

  1. Consistent Naming: Use XXXMode for struct names, NewXXXMode for constructors
  2. Handler Reference: Always store a reference to the Handler for accessing services
  3. State Management: Use the Handler's state management methods
  4. Logging: Log mode transitions and important events
  5. Error Handling: Handle errors gracefully, don't panic
  6. Resource Cleanup: Always clean up overlays and state in Exit()
  7. Action Mode Support: Implement ToggleActionMode() even if not used

Adding New Modes

To add a new navigation mode:

  1. Define Domain Mode: Add to internal/core/domain/modes.go
  2. Create Implementation: Implement the Mode interface
  3. Add CLI Command: Create CLI command in internal/cli/
  4. Update IPC: Add handler in internal/app/ipc_controller.go
  5. Register Mode: Add to Handler's mode map
  6. Add Tests: Create unit and integration tests
  7. Update Config: Add hotkey defaults
  8. Update Docs: Document in CLI.md and DEVELOPMENT.md

Key Technologies

  • Go - Core application logic, CLI, configuration
  • CGo + Objective-C - macOS Accessibility API integration
  • Cobra - CLI framework
  • TOML - Configuration format
  • Unix Sockets - IPC communication

Architectural Layers

Neru follows clean architecture with clear separation of concerns:

Domain Layer (internal/core/domain)

Pure business logic with no external dependencies:

  • Entities: Core concepts (Hint, Grid, Element, Action)
  • Value Objects: Immutable data structures
  • Business Rules: Domain logic and validation

Ports Layer (internal/core/ports)

Interfaces defining contracts between layers:

  • AccessibilityPort: UI element access and interaction
  • OverlayPort: UI overlay management
  • ConfigPort: Configuration management
  • InfrastructurePort: System-level operations

Application Layer (internal/app)

Implements use cases and orchestrates domain entities:

  • Services: Business logic orchestration (HintService, GridService, ActionService)
  • Components: UI components for Hints, Grid, and Scroll modes
  • Modes: Navigation mode implementations following the Mode interface
  • Lifecycle: Application startup, shutdown, and orchestration

Infrastructure Layer (internal/core/infra)

Concrete implementations of ports:

  • Accessibility: Platform accessibility API integration (AXUIElement on macOS)
  • Overlay: UI overlay management and rendering
  • Config: Configuration loading and parsing
  • EventTap: Global input monitoring
  • Hotkeys: System hotkey registration
  • IPC: Inter-process communication
  • Platform: OS-specific adapters (platform/darwin, platform/linux, platform/windows)

Presentation Layer (internal/ui)

User interface rendering:

  • UI: Overlay rendering and coordinate conversion

Data Flow

  1. Startup: Configuration is loaded → Dependencies are wired → Hotkeys registered → App waits for input
  2. User Interaction: Hotkey pressed → Event tap captures → Mode activated → UI overlays displayed
  3. Processing: User input processed → Actions determined → System APIs called → Results rendered
  4. Cleanup: Mode exited → Overlays hidden → State reset → App returns to idle

Core Packages

internal/core/domain

Core business logic and entities (pure Go, no external dependencies):

  • Element: UI element representation with bounds, role, and state
  • Hint/Grid/Action: Navigation and interaction primitives

internal/core/ports

Interface contracts between layers:

  • AccessibilityPort: UI element access and interaction
  • OverlayPort: UI overlay management
  • ConfigPort: Configuration management
  • InfrastructurePort: System-level operations

internal/app

Application orchestration and use cases:

  • Services: Business logic orchestration (HintService, GridService, ActionService)
  • Components: UI components for Hints, Grid, and Scroll modes
  • Modes: Navigation mode implementations following the Mode interface
  • App: Central application state and dependencies
  • Lifecycle: Startup, shutdown, and orchestration

internal/core/infra

Infrastructure implementations:

  • Accessibility: Platform accessibility API integration (AXUIElement on macOS)
  • Overlay: UI overlay management and rendering
  • Config: Configuration loading and parsing
  • EventTap: Global input monitoring
  • Hotkeys: System hotkey registration
  • IPC: Inter-process communication
  • Platform: OS-specific adapters (platform/darwin, platform/linux, platform/windows)

internal/ui

Presentation layer:

  • UI: Overlay rendering and coordinate conversion

internal/cli

Command-line interface (Cobra-based):

  • Command parsing and dispatch
  • Output formatting and error handling

internal/config

Configuration management:

  • TOML parsing and validation
  • Multi-location config loading
  • Default value provision

Where to Add New Code

Configuration Options:

  1. Add fields to internal/config/config.go structs
  2. Update commonDefaultConfig() with shared defaults; add platform-specific defaults to internal/config/config_<os>.go
  3. Add validation in Validate*() methods
  4. Update configs/ examples and docs/CONFIGURATION.md

Navigation Modes:

  1. Define domain entities in internal/core/domain/
  2. Create service in internal/app/services/
  3. Implement infrastructure in internal/core/infra/
  4. Add components in internal/app/components/
  5. Implement the Mode interface in internal/app/modes/ (see HintsMode, GridMode, ScrollMode for examples)
    • See also RecursiveGridMode for recursive grid navigation
  6. Register mode in the Handler's mode map in internal/app/modes/handler.go

Actions:

  1. Define action in internal/core/domain/action/
  2. Implement logic in internal/app/services/action_service.go
  3. Add handling in internal/app/modes/actions.go
  4. Update config and documentation

UI Components:

  1. Create components in internal/app/components/
  2. Implement rendering in internal/ui/
  3. Add macOS-specific Objective-C in internal/core/infra/platform/darwin/ with a //go:build darwin tag; provide a no-op stub for other platforms
  4. Register in internal/app/component_factory.go or internal/app/app_initialization.go

CLI Commands:

  1. Create command file in internal/cli/
  2. Register in internal/cli/root.go
  3. Document in docs/CLI.md

Dependency Injection and Wiring

Neru uses manual dependency injection for better testability and explicit dependency management:

  1. Construction: Dependencies are explicitly passed to constructors
  2. Wiring: internal/app/app_initialization.go wires all components together
  3. Testing: Dependencies can be mocked by passing test doubles in NewWithDeps
  4. Ports: Interfaces define contracts between layers

Example of dependency injection in action:

// In internal/app/initialization.go
hintService := services.NewHintService(accAdapter, overlayAdapter, systemPort, hintGen, cfg.Hints, logger)
gridService := services.NewGridService(overlayAdapter, systemPort, logger)
actionService := services.NewActionService(accAdapter, overlayAdapter, systemPort, logger)

Contributing

Development Workflow

  1. Fork and clone the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Make changes following Coding Standards
  4. Add tests for new functionality
  5. Test thoroughly: just test && just lint && just build
  6. Commit conventionally: git commit -m "feat: description"
  7. Push and open PR with description and screenshots

Before You Start

  • Read the Architecture: Understand layered design and code placement
  • Check Existing Issues: Search for similar work or start discussions
  • Follow Standards: See CODING_STANDARDS.md
  • Write Tests: All new code needs appropriate test coverage
  • Update Docs: Keep documentation current with changes

Code Standards

All code must follow the Coding Standards document. See Testing Standards for test requirements.

Pre-commit Checklist:

  • Code formatted (just fmt)
  • Linters pass (just lint)
  • Tests pass (just test)
  • Build succeeds (just build)
  • Documentation updated
  • Follows CODING_STANDARDS.md

Key Requirements:

  • Use goimports for import organization
  • Add godoc comments for exported symbols
  • Use custom error package with proper wrapping
  • Follow established naming patterns and receiver conventions

Testing Guidelines

All new code requires appropriate tests. See CODING_STANDARDS.md for detailed guidelines.

Test Types:

  • Unit Tests: Business logic, algorithms, validation (fast, no system deps)
  • Integration Tests: Real platform APIs, file system, IPC (tagged //go:build integration && <os>)

When to Use:

  • Unit Tests: Business logic, config validation, component interfaces, pure algorithms
  • Integration Tests: Platform APIs, file operations, IPC, component coordination

Test Organization:

package_test.go                        # Unit tests (logic, mocks)
package_integration_darwin_test.go     # macOS integration tests //go:build integration && darwin
package_integration_linux_test.go      # Linux integration tests  //go:build integration && linux

Documentation

  • Update docs/ for significant changes
  • Add godoc comments for exported symbols
  • Keep docs consistent with code changes
  • Include examples where helpful

Commit Messages

Use clear, descriptive commit messages following conventional commits:

Format: <type>(<scope>): <subject>

Types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • style: Code style changes (formatting, etc.)
  • refactor: Code refactoring
  • perf: Performance improvements
  • test: Adding or updating tests
  • chore: Build process, dependencies, etc.

Good:

feat: add grid-based navigation mode

Implement grid-based navigation as an alternative to hint-based navigation.
Grid mode divides the screen into cells and allows precise cursor positioning.

Closes #123

Bad:

fix bug

Release Process

Releases are being handled via Release Please automatically.

Version Numbering

Neru uses semantic versioning: vMAJOR.MINOR.PATCH

  • MAJOR - Breaking changes
  • MINOR - New features (backward compatible)
  • PATCH - Bug fixes

Creating a Release

Creating a release is just as easy as merging the release please PR, and it will build and publish the binaries on github.

Note

Homebrew version bump is in a separate repo, it will be updated separately.


Development Tips

Quick Iteration

# Build and run
just build && ./bin/neru launch

# Watch for changes (requires entr)
ls **/*.go | entr -r sh -c 'just build && ./bin/neru launch'

Debugging

# Enable debug logging in config
[logging]
log_level = "debug"

# Watch logs (macOS)
tail -f ~/Library/Logs/neru/app.log

# Watch logs (Linux)
tail -f ~/.local/state/neru/log/app.log

Useful Commands

# Code quality
just fmt          # Format code
just lint         # Run linters
just test         # Run tests

# Dependencies
go mod tidy       # Clean up modules
go get -u ./...   # Update dependencies

Troubleshooting

  • Read existing code - Well-structured codebase
  • Check issues - Search for similar problems
  • Ask in discussions - Open GitHub discussion for questions
  • Open draft PR - Get early feedback on approach

Resources