This document describes the architecture of claws.
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) │
└─────────────────────────────────────────────────────────────┘
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
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")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
}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., cfn → cloudformation, sfn → stepfunctions)
Sub-Resources: Resources only accessible via navigation (e.g., cloudformation/events)
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) |
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)
}claws supports querying multiple AWS regions simultaneously via the R key.
┌─────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────┘
RegionalDAOWrapper (internal/registry/wrapper.go):
- Automatically wraps all DAOs when region override is present in context
dao.WrapWithRegion(resource, region)adds region metadatadao.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
}- User selects multiple regions via
Rkey ResourceBrowser.fetchMultiRegionResources()spawns goroutines per region- Each goroutine:
aws.WithRegionOverride(ctx, region)→GetDAO()→List() GetDAO()auto-wraps DAO withRegionalDAOWrapper- Wrapper calls underlying DAO, wraps each resource with region
- Results collected, merged, displayed with Region column
- Before rendering/actions,
dao.UnwrapResource()retrieves concrete type
If some regions fail (access denied, timeout, etc.):
- Successful regions display normally
- Errors logged at WARN level
- User sees partial results without disruption
The internal/aws/ package provides essential helpers:
cfg, err := appaws.NewConfig(ctx) // Load AWS config from environment// 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
}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 limitingname := 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.TimeAll 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| 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) |
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 (
viewStackremains unchanged) - Support nesting via modal stack (e.g., Profile Selector → Profile Detail)
- Dismissed with
esc,q, orbackspace - 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 = 70ModalWidthRegion = 45ModalWidthProfile = 55ModalWidthProfileDetail = 65ModalWidthActionMenu = 60
Application configuration is stored in ~/.config/claws/config.yaml:
startup:
profiles:
- my-aws-profile
regions:
- us-east-1
theme: nordAWS credentials and config are read from standard locations:
~/.aws/credentials~/.aws/config- Environment variables
- 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
PaginatedDAOwithNkey for next page
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.