Skip to content

Latest commit

 

History

History
382 lines (307 loc) · 14.8 KB

File metadata and controls

382 lines (307 loc) · 14.8 KB

Architecture

This document describes the architecture of claws.

Overview

claws is built with a modular architecture that separates concerns into distinct layers:

┌─────────────────────────────────────────────────────────────┐
│                         TUI Layer                           │
│  (Bubbletea App, Views, Rendering)                         │
├─────────────────────────────────────────────────────────────┤
│                      Registry Layer                         │
│  (Service/Resource registration, alias resolution)          │
├─────────────────────────────────────────────────────────────┤
│                     Business Layer                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    custom/                           │   │
│  │  (Service implementations: DAO + Renderer)          │   │
│  └─────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────┤
│                       AWS SDK Layer                         │
│  (AWS SDK for Go v2)                                       │
└─────────────────────────────────────────────────────────────┘

Directory Structure

claws/
├── cmd/claws/              # Application entry point
├── internal/
│   ├── app/                # Main Bubbletea application
│   ├── aws/                # AWS client management and helpers
│   │   ├── client.go       # NewConfig() for AWS config loading
│   │   ├── paginate.go     # Paginate(), PaginateIter() helpers
│   │   ├── errors.go       # IsNotFound(), IsAccessDenied(), etc.
│   │   └── pointers.go     # Str(), Int32(), Int64(), Time() helpers
│   ├── action/             # Action framework (API calls, exec commands)
│   ├── config/             # Application configuration (profile, region)
│   ├── dao/                # Data Access Object interface + context filtering
│   ├── registry/           # Service/resource registration + aliases
│   ├── render/             # Renderer interface, DetailBuilder, Navigation
│   ├── ui/                 # Theme system and UI utilities
│   └── view/               # View components (browser, detail, command, help)
├── custom/                 # All 65 service implementations
│   ├── ec2/                # EC2 (13 resources)
│   ├── iam/                # IAM (5 resources)
│   ├── glue/               # Glue (5 resources)
│   ├── bedrock/            # Bedrock (3 resources)
│   ├── bedrockagent/       # Bedrock Agent (5 resources)
│   ├── sagemaker/          # SageMaker (4 resources)
│   └── ...                 # 59 more services

Core Concepts

DAO (Data Access Object)

The DAO interface provides data access for AWS resources:

type DAO interface {
    ServiceName() string
    ResourceType() string
    List(ctx context.Context) ([]Resource, error)
    Get(ctx context.Context, id string) (Resource, error)
    Delete(ctx context.Context, id string) error
}

Each resource type implements this interface. The BaseDAO struct provides default implementations for common methods.

PaginatedDAO: For large datasets, implement the optional PaginatedDAO interface:

type PaginatedDAO interface {
    DAO
    ListPage(ctx context.Context, pageSize int, pageToken string) ([]Resource, string, error)
}

Context Filtering: DAOs can receive filter parameters via context:

// Set filter in context
ctx = dao.WithFilter(ctx, "VpcId", "vpc-12345")

// Retrieve filter in DAO
vpcId := dao.GetFilterFromContext(ctx, "VpcId")

Renderer

The Renderer interface handles UI rendering:

type Renderer interface {
    Columns() []Column
    RenderRow(resource dao.Resource) table.Row
    RenderDetail(resource dao.Resource) string
    RenderSummary(resource dao.Resource) []SummaryField
}

Navigator: Optional interface for cross-resource navigation:

type Navigator interface {
    Navigations(resource dao.Resource) []Navigation
}

Registry

The registry manages service/resource registrations:

registry.Global.RegisterCustom("ec2", "instances", registry.Entry{
    DAOFactory:      func(ctx context.Context) (dao.DAO, error) { ... },
    RendererFactory: func() render.Renderer { ... },
})

Service Aliases: Short names for common services (e.g., cfncloudformation, sfnstepfunctions)

Sub-Resources: Resources only accessible via navigation (e.g., cloudformation/events)

Actions

Actions define operations that can be performed on resources:

Type Description
api AWS API call (e.g., StopInstances)
exec Execute shell command (e.g., SSH)
view Navigate to another view

Actions are defined in Go code within each resource's actions.go file:

func init() {
    action.Global.Register("ec2", "instances", []action.Action{
        {Name: "Stop Instance", Shortcut: "S", Type: action.ActionTypeAPI, Confirm: action.ConfirmSimple},
        {Name: "SSH", Shortcut: "s", Type: action.ActionTypeExec},
    })
    action.RegisterExecutor("ec2", "instances", ExecuteAction)
}

ConfirmLevel: Actions can specify confirmation requirements:

Level Description
ConfirmNone No confirmation (default)
ConfirmSimple Yes/No confirmation
ConfirmDangerous Requires typing resource ID (destructive actions)

Navigation

Resources can define navigation shortcuts to related resources:

type Navigation struct {
    Key         string        // Shortcut key (e.g., "v")
    Label       string        // Display label (e.g., "VPC")
    Service     string        // Target service
    Resource    string        // Target resource type
    FilterField string        // Filter field name (e.g., "VpcId")
    FilterValue string        // Filter value (extracted from current resource)
    AutoReload  bool          // Auto-refresh (for events, logs)
}

Multi-Region Support

claws supports querying multiple AWS regions simultaneously via the R key.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    View Layer                               │
│  ResourceBrowser detects multi-region, spawns goroutines    │
├─────────────────────────────────────────────────────────────┤
│                   Registry Layer                            │
│  GetDAO() auto-wraps with RegionalDAOWrapper                │
├─────────────────────────────────────────────────────────────┤
│                   Wrapper Layer                             │
│  RegionalDAOWrapper / PaginatedDAOWrapper                   │
│  - Wraps resources with region metadata                     │
│  - Preserves concrete types for rendering                   │
├─────────────────────────────────────────────────────────────┤
│                    DAO Layer                                │
│  164 custom DAOs - unmodified, region-agnostic              │
└─────────────────────────────────────────────────────────────┘

Key Components

RegionalDAOWrapper (internal/registry/wrapper.go):

  • Automatically wraps all DAOs when region override is present in context
  • dao.WrapWithRegion(resource, region) adds region metadata
  • dao.UnwrapResource(resource) retrieves original for type assertions
  • Backward compatible: no wrapping when single-region mode

Parallel Fetching (internal/view/resource_browser.go):

func (r *ResourceBrowser) fetchMultiRegionResources(regions []string, ...) {
    results := make(chan regionResult, len(regions))
    for _, region := range regions {
        go func(region string) {
            regionCtx := aws.WithRegionOverride(r.ctx, region)
            d, _ := r.registry.GetDAO(regionCtx, service, resourceType)
            resources, _ := d.List(regionCtx)
            results <- regionResult{region, resources, nil}
        }(region)
    }
    // Collect results, handle partial failures
}

Double-Wrap Prevention (internal/registry/registry.go):

// GetDAO checks if delegate is already wrapped
if _, ok := delegate.(*RegionalDAOWrapper); ok {
    return delegate, nil
}
if _, ok := delegate.(*PaginatedDAOWrapper); ok {
    return delegate, nil
}

Resource Flow

  1. User selects multiple regions via R key
  2. ResourceBrowser.fetchMultiRegionResources() spawns goroutines per region
  3. Each goroutine: aws.WithRegionOverride(ctx, region)GetDAO()List()
  4. GetDAO() auto-wraps DAO with RegionalDAOWrapper
  5. Wrapper calls underlying DAO, wraps each resource with region
  6. Results collected, merged, displayed with Region column
  7. Before rendering/actions, dao.UnwrapResource() retrieves concrete type

Partial Failure Handling

If some regions fail (access denied, timeout, etc.):

  • Successful regions display normally
  • Errors logged at WARN level
  • User sees partial results without disruption

AWS Helper Functions

The internal/aws/ package provides essential helpers:

Config Loading

cfg, err := appaws.NewConfig(ctx)  // Load AWS config from environment

Pagination

// Batch pagination - collects all results
items, err := appaws.Paginate(ctx, func(token *string) ([]Item, *string, error) {
    output, err := client.ListItems(ctx, &ListItemsInput{NextToken: token})
    if err != nil {
        return nil, nil, err
    }
    return output.Items, output.NextToken, nil
})

// Streaming pagination - processes items one at a time
for item := range appaws.PaginateIter(ctx, fetchFunc) {
    // Process item
}

Error Handling

if appaws.IsNotFound(err) { }      // Check for "not found" errors
if appaws.IsAccessDenied(err) { }  // Check for "access denied" errors
if appaws.IsThrottling(err) { }    // Check for rate limiting

Safe Pointer Dereferencing

name := appaws.Str(item.Name)        // *string → string
count := appaws.Int32(item.Count)    // *int32 → int32
size := appaws.Int64(item.Size)      // *int64 → int64
created := appaws.Time(item.Created) // *time.Time → time.Time

Theme System

All UI colors are centralized in internal/ui/theme.go:

t := ui.Current()           // Get current theme
ui.DimStyle()               // Helper for dim text
ui.SuccessStyle()           // Helper for success color
ui.WarningStyle()           // Helper for warning color
ui.DangerStyle()            // Helper for error color

Views

View Description
Service Browser List of available AWS services
Resource Browser Table view of resources with filtering and sorting
Detail View Detailed resource information with scrolling
Command Mode : command input for navigation and sorting
Filter Mode / search input for filtering
Help View ? key bindings reference (modal)
Action Menu a available actions for resource (modal)
Region Selector R AWS region switching (modal)
Profile Selector P AWS profile switching (modal)

Modal System

Some views (Help, Region Selector, Profile Selector, Action Menu) display as modals that overlay the current view rather than pushing to the view stack.

Key Characteristics:

  • Modals don't affect the view stack (viewStack remains unchanged)
  • Support nesting via modal stack (e.g., Profile Selector → Profile Detail)
  • Dismissed with esc, q, or backspace
  • Automatically cleared on region/profile change

Modal Stack Flow:

┌─────────────────────────────────────────────────────────────┐
│  ShowModalMsg    →  Push current modal to stack, show new   │
│  HideModalMsg    →  Pop stack (restore previous or close)   │
│  NavigateMsg     →  Clear stack, close all modals           │
│  Region/Profile  →  Clear stack, refresh underlying view    │
└─────────────────────────────────────────────────────────────┘

Width Constants (internal/view/modal.go):

  • ModalWidthHelp = 70
  • ModalWidthRegion = 45
  • ModalWidthProfile = 55
  • ModalWidthProfileDetail = 65
  • ModalWidthActionMenu = 60

Configuration

Application configuration is stored in ~/.config/claws/config.yaml:

startup:
  profiles:
    - my-aws-profile
  regions:
    - us-east-1
theme: nord

AWS credentials and config are read from standard locations:

  • ~/.aws/credentials
  • ~/.aws/config
  • Environment variables

Performance Optimizations

  • Style Caching: Lipgloss styles are cached in struct fields to avoid per-frame allocations
  • Lazy Loading: Resources are loaded on-demand when navigating to a service
  • Pagination: Large result sets use AWS SDK pagination with appaws.Paginate
  • Manual Pagination: For very large datasets, use PaginatedDAO with N key for next page

Logging

Structured logging via internal/log/:

log.Debug("operation completed", "duration", elapsed)
log.Info("action executed", "service", svc, "resource", res)
log.Warn("resource not found", "id", id)
log.Error("failed", "error", err)

Logs are only written when -l/--log-file is specified at startup.