sqs-browse: a lot of work to try to keep UI complexity down
Added the notion of controllers and a dispatcher which will queue up operations
This commit is contained in:
		
							parent
							
								
									1969504611
								
							
						
					
					
						commit
						7526c095ee
					
				|  | @ -7,6 +7,8 @@ import ( | |||
| 	"github.com/aws/aws-sdk-go-v2/config" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/dispatcher" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/ui" | ||||
|  | @ -38,7 +40,12 @@ func main() { | |||
| 	tableService := tables.NewService(dynamoProvider) | ||||
| 
 | ||||
| 	loopback := &msgLoopback{} | ||||
| 	uiModel := ui.NewModel(tableService, loopback, *flagTable) | ||||
| 	uiDispatcher := dispatcher.NewDispatcher(loopback) | ||||
| 
 | ||||
| 	tableReadController := controllers.NewTableReadController(tableService, *flagTable) | ||||
| 	tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, *flagTable) | ||||
| 
 | ||||
| 	uiModel := ui.NewModel(uiDispatcher, tableReadController, tableWriteController) | ||||
| 	p := tea.NewProgram(uiModel, tea.WithAltScreen()) | ||||
| 	loopback.program = p | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,9 +7,12 @@ import ( | |||
| 	"github.com/aws/aws-sdk-go-v2/config" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/sqs" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/dispatcher" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/controllers" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/models" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/providers/memstore" | ||||
| 	sqsprovider "github.com/lmika/awstools/internal/sqs-browse/providers/sqs" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/services/messages" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/services/pollmessage" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/ui" | ||||
| 	"github.com/lmika/events" | ||||
|  | @ -20,6 +23,7 @@ import ( | |||
| 
 | ||||
| func main() { | ||||
| 	var flagQueue = flag.String("q", "", "queue to poll") | ||||
| 	var flagTarget = flag.String("t", "", "target queue to push to") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
|  | @ -32,12 +36,19 @@ func main() { | |||
| 	bus := events.New() | ||||
| 
 | ||||
| 	msgStore := memstore.NewStore() | ||||
| 	msgPoller := sqsprovider.NewProvider(sqsClient) | ||||
| 	sqsProvider := sqsprovider.NewProvider(sqsClient) | ||||
| 
 | ||||
| 	pollService := pollmessage.NewService(msgStore, msgPoller, *flagQueue, bus) | ||||
| 	messageService := messages.NewService(sqsProvider) | ||||
| 	pollService := pollmessage.NewService(msgStore, sqsProvider, *flagQueue, bus) | ||||
| 
 | ||||
| 	uiModel := ui.NewModel() | ||||
| 	msgSendingHandlers := controllers.NewMessageSendingController(messageService, *flagTarget) | ||||
| 
 | ||||
| 	loopback := &msgLoopback{} | ||||
| 	uiDispatcher := dispatcher.NewDispatcher(loopback) | ||||
| 
 | ||||
| 	uiModel := ui.NewModel(uiDispatcher, msgSendingHandlers) | ||||
| 	p := tea.NewProgram(uiModel, tea.WithAltScreen()) | ||||
| 	loopback.program = p | ||||
| 
 | ||||
| 	bus.On("new-messages", func(m []*models.Message) { p.Send(ui.NewMessagesEvent(m)) }) | ||||
| 
 | ||||
|  | @ -58,4 +69,12 @@ func main() { | |||
| 		fmt.Printf("Alas, there's been an error: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| } | ||||
| } | ||||
| 
 | ||||
| type msgLoopback struct { | ||||
| 	program *tea.Program | ||||
| } | ||||
| 
 | ||||
| func (m *msgLoopback) Send(msg tea.Msg) { | ||||
| 	m.program.Send(msg) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							|  | @ -3,6 +3,7 @@ module github.com/lmika/awstools | |||
| go 1.17 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/atotto/clipboard v0.1.4 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2 v1.15.0 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2/config v1.13.1 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect | ||||
|  |  | |||
							
								
								
									
										1
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.sum
									
									
									
									
									
								
							|  | @ -1,3 +1,4 @@ | |||
| github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= | ||||
| github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= | ||||
| github.com/aws/aws-sdk-go-v2 v1.13.0 h1:1XIXAfxsEmbhbj5ry3D3vX+6ZcUYvIqSm4CWWEuGZCA= | ||||
| github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w= | ||||
|  |  | |||
							
								
								
									
										30
									
								
								internal/common/ui/dispatcher/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								internal/common/ui/dispatcher/context.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| package dispatcher | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/events" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| ) | ||||
| 
 | ||||
| type dispatcherContext struct { | ||||
| 	d *Dispatcher | ||||
| } | ||||
| 
 | ||||
| func (dc dispatcherContext) Messagef(format string, args ...interface{}) { | ||||
| 	dc.d.publisher.Send(events.Message(fmt.Sprintf(format, args...))) | ||||
| } | ||||
| 
 | ||||
| func (dc dispatcherContext) Send(teaMessage tea.Msg) { | ||||
| 	dc.d.publisher.Send(teaMessage) | ||||
| } | ||||
| 
 | ||||
| func (dc dispatcherContext) Message(msg string) { | ||||
| 	dc.d.publisher.Send(events.Message(msg)) | ||||
| } | ||||
| 
 | ||||
| func (dc dispatcherContext) Input(prompt string, onDone uimodels.Operation) { | ||||
| 	dc.d.publisher.Send(events.PromptForInput{ | ||||
| 		OnDone: onDone, | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										49
									
								
								internal/common/ui/dispatcher/dispatcher.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								internal/common/ui/dispatcher/dispatcher.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| package dispatcher | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/events" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| type Dispatcher struct { | ||||
| 	mutex     *sync.Mutex | ||||
| 	runningOp uimodels.Operation | ||||
| 	publisher MessagePublisher | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| func NewDispatcher(publisher MessagePublisher) *Dispatcher { | ||||
| 	return &Dispatcher{ | ||||
| 		mutex:     new(sync.Mutex), | ||||
| 		publisher: publisher, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (d *Dispatcher) Start(ctx context.Context, operation uimodels.Operation) { | ||||
| 	d.mutex.Lock() | ||||
| 	defer d.mutex.Unlock() | ||||
| 
 | ||||
| 	if d.runningOp != nil { | ||||
| 		d.publisher.Send(events.Error(errors.New("operation already running"))) | ||||
| 	} | ||||
| 
 | ||||
| 	d.runningOp = operation | ||||
| 	go func() { | ||||
| 		subCtx := uimodels.WithContext(ctx, dispatcherContext{d}) | ||||
| 
 | ||||
| 		err := operation.Execute(subCtx) | ||||
| 		if err != nil { | ||||
| 			d.publisher.Send(events.Error(err)) | ||||
| 		} | ||||
| 
 | ||||
| 		d.mutex.Lock() | ||||
| 		defer d.mutex.Unlock() | ||||
| 		d.runningOp = nil | ||||
| 	}() | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										7
									
								
								internal/common/ui/dispatcher/iface.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								internal/common/ui/dispatcher/iface.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| package dispatcher | ||||
| 
 | ||||
| import tea "github.com/charmbracelet/bubbletea" | ||||
| 
 | ||||
| type MessagePublisher interface { | ||||
| 	Send(msg tea.Msg) | ||||
| } | ||||
							
								
								
									
										16
									
								
								internal/common/ui/events/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								internal/common/ui/events/errors.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| package events | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| ) | ||||
| 
 | ||||
| // Error indicates that an error occurred
 | ||||
| type Error error | ||||
| 
 | ||||
| // Message indicates that a message should be shown to the user
 | ||||
| type Message string | ||||
| 
 | ||||
| // PromptForInput indicates that the context is requesting a line of input
 | ||||
| type PromptForInput struct { | ||||
| 	OnDone uimodels.Operation | ||||
| } | ||||
							
								
								
									
										15
									
								
								internal/common/ui/uimodels/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/common/ui/uimodels/context.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| package uimodels | ||||
| 
 | ||||
| import "context" | ||||
| 
 | ||||
| type uiContextKeyType struct {} | ||||
| var uiContextKey = uiContextKeyType{} | ||||
| 
 | ||||
| func Ctx(ctx context.Context) UIContext { | ||||
| 	uiCtx, _ := ctx.Value(uiContextKey).(UIContext) | ||||
| 	return uiCtx | ||||
| } | ||||
| 
 | ||||
| func WithContext(ctx context.Context, uiContext UIContext) context.Context { | ||||
| 	return context.WithValue(ctx, uiContextKey, uiContext) | ||||
| } | ||||
							
								
								
									
										10
									
								
								internal/common/ui/uimodels/iface.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								internal/common/ui/uimodels/iface.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| package uimodels | ||||
| 
 | ||||
| import tea "github.com/charmbracelet/bubbletea" | ||||
| 
 | ||||
| type UIContext interface { | ||||
| 	Send(teaMessage tea.Msg) | ||||
| 	Message(msg string) | ||||
| 	Messagef(format string, args ...interface{}) | ||||
| 	Input(prompt string, onDone Operation) | ||||
| } | ||||
							
								
								
									
										14
									
								
								internal/common/ui/uimodels/operations.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								internal/common/ui/uimodels/operations.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| package uimodels | ||||
| 
 | ||||
| import "context" | ||||
| 
 | ||||
| type Operation interface { | ||||
| 	Execute(ctx context.Context) error | ||||
| } | ||||
| 
 | ||||
| type OperationFn func(ctx context.Context) error | ||||
| 
 | ||||
| func (f OperationFn) Execute(ctx context.Context) error { | ||||
| 	return f(ctx) | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										15
									
								
								internal/common/ui/uimodels/promptvalue.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/common/ui/uimodels/promptvalue.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| package uimodels | ||||
| 
 | ||||
| import "context" | ||||
| 
 | ||||
| type promptValueKeyType struct {} | ||||
| var promptValueKey = promptValueKeyType{} | ||||
| 
 | ||||
| func PromptValue(ctx context.Context) string { | ||||
| 	value, _ := ctx.Value(promptValueKey).(string) | ||||
| 	return value | ||||
| } | ||||
| 
 | ||||
| func WithPromptValue(ctx context.Context, value string) context.Context { | ||||
| 	return context.WithValue(ctx, promptValueKey, value) | ||||
| } | ||||
|  | @ -1,10 +1,7 @@ | |||
| package ui | ||||
| package controllers | ||||
| 
 | ||||
| import "github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| 
 | ||||
| type newResultSet struct { | ||||
| type NewResultSet struct { | ||||
| 	ResultSet *models.ResultSet | ||||
| } | ||||
| 
 | ||||
| type setStatusMessage string | ||||
| type errorRaised error | ||||
							
								
								
									
										44
									
								
								internal/dynamo-browse/controllers/tableread.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								internal/dynamo-browse/controllers/tableread.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | |||
| package controllers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/services/tables" | ||||
| ) | ||||
| 
 | ||||
| type TableReadController struct { | ||||
| 	tableService *tables.Service | ||||
| 	tableName string | ||||
| } | ||||
| 
 | ||||
| func NewTableReadController(tableService *tables.Service, tableName string) *TableReadController { | ||||
| 	return &TableReadController{ | ||||
| 		tableService: tableService, | ||||
| 		tableName: tableName, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (c *TableReadController) Scan() uimodels.Operation { | ||||
| 	return uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 		return c.doScan(ctx, false) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (c *TableReadController) doScan(ctx context.Context, quiet bool) error { | ||||
| 	uiCtx := uimodels.Ctx(ctx) | ||||
| 
 | ||||
| 	if !quiet { | ||||
| 		uiCtx.Message("Scanning...") | ||||
| 	} | ||||
| 
 | ||||
| 	resultSet, err := c.tableService.Scan(ctx, c.tableName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if !quiet { | ||||
| 		uiCtx.Messagef("Found %d items", len(resultSet.Items)) | ||||
| 	} | ||||
| 	uiCtx.Send(NewResultSet{resultSet}) | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										53
									
								
								internal/dynamo-browse/controllers/tablewrite.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								internal/dynamo-browse/controllers/tablewrite.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| package controllers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
| 
 | ||||
| type TableWriteController struct { | ||||
| 	tableService *tables.Service | ||||
| 	tableReadControllers *TableReadController | ||||
| 	tableName string | ||||
| } | ||||
| 
 | ||||
| func NewTableWriteController(tableService *tables.Service, tableReadControllers *TableReadController, tableName string) *TableWriteController { | ||||
| 	return &TableWriteController{ | ||||
| 		tableService: tableService, | ||||
| 		tableReadControllers: tableReadControllers, | ||||
| 		tableName: tableName, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (c *TableWriteController) Delete(item models.Item) uimodels.Operation { | ||||
| 	return uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 		uiCtx := uimodels.Ctx(ctx) | ||||
| 
 | ||||
| 		// TODO: only do if rw mode enabled
 | ||||
| 
 | ||||
| 		uiCtx.Input("Delete item?", uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 			uiCtx := uimodels.Ctx(ctx) | ||||
| 
 | ||||
| 			if uimodels.PromptValue(ctx) != "y" { | ||||
| 				return errors.New("operation aborted") | ||||
| 			} | ||||
| 
 | ||||
| 			// Delete the item
 | ||||
| 			if err := c.tableService.Delete(ctx, c.tableName, item); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			// Rescan to get updated items
 | ||||
| 			if err := c.tableReadControllers.doScan(ctx, true); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			uiCtx.Message("Item deleted") | ||||
| 			return nil | ||||
| 		})) | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
|  | @ -4,6 +4,7 @@ import ( | |||
| 	"context" | ||||
| 	"github.com/aws/aws-sdk-go-v2/aws" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
|  | @ -31,3 +32,11 @@ func (p *Provider) ScanItems(ctx context.Context, tableName string) ([]models.It | |||
| 
 | ||||
| 	return items, nil | ||||
| } | ||||
| 
 | ||||
| func (p *Provider) DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error { | ||||
| 	_, err := p.client.DeleteItem(ctx, &dynamodb.DeleteItemInput{ | ||||
| 		TableName: aws.String(tableName), | ||||
| 		Key: key, | ||||
| 	}) | ||||
| 	return errors.Wrap(err, "could not delete item") | ||||
| } | ||||
|  | @ -2,9 +2,11 @@ package tables | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| ) | ||||
| 
 | ||||
| type TableProvider interface { | ||||
| 	ScanItems(ctx context.Context, tableName string) ([]models.Item, error) | ||||
| 	DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error | ||||
| } | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ package tables | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"sort" | ||||
|  | @ -50,3 +51,11 @@ func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, er | |||
| 		Items: results, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (s *Service) Delete(ctx context.Context, name string, item models.Item) error { | ||||
| 	// TODO: do not hardcode keys
 | ||||
| 	return s.provider.DeleteItem(ctx, name, map[string]types.AttributeValue{ | ||||
| 		"pk": item["pk"], | ||||
| 		"sk": item["sk"], | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
|  | @ -5,42 +5,59 @@ import ( | |||
| 	"fmt" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	table "github.com/calyptia/go-bubble-table" | ||||
| 	"github.com/charmbracelet/bubbles/textinput" | ||||
| 	"github.com/charmbracelet/bubbles/viewport" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/charmbracelet/lipgloss" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/dispatcher" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/events" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/services/tables" | ||||
| 	"log" | ||||
| 	"strings" | ||||
| 	"text/tabwriter" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	headerStyle = lipgloss.NewStyle(). | ||||
| 		Bold(true). | ||||
| 		Foreground(lipgloss.Color("#ffffff")). | ||||
| 		Background(lipgloss.Color("#4479ff")) | ||||
| ) | ||||
| 
 | ||||
| type uiModel struct { | ||||
| 	table    table.Model | ||||
| 	viewport viewport.Model | ||||
| 
 | ||||
| 	msgPublisher MessagePublisher | ||||
| 	tableService *tables.Service | ||||
| 	tableName    string | ||||
| 
 | ||||
| 	tableWidth, tableHeight int | ||||
| 
 | ||||
| 	ready     bool | ||||
| 	resultSet *models.ResultSet | ||||
| 	message   string | ||||
| 
 | ||||
| 	pendingInput *events.PromptForInput | ||||
| 	textInput    textinput.Model | ||||
| 
 | ||||
| 	dispatcher           *dispatcher.Dispatcher | ||||
| 	tableReadController  *controllers.TableReadController | ||||
| 	tableWriteController *controllers.TableWriteController | ||||
| } | ||||
| 
 | ||||
| func NewModel(tableService *tables.Service, msgPublisher MessagePublisher, tableName string) tea.Model { | ||||
| func NewModel(dispatcher *dispatcher.Dispatcher, tableReadController *controllers.TableReadController, tableWriteController *controllers.TableWriteController) tea.Model { | ||||
| 	tbl := table.New([]string{"pk", "sk"}, 100, 20) | ||||
| 	rows := make([]table.Row, 0) | ||||
| 	tbl.SetRows(rows) | ||||
| 
 | ||||
| 	textInput := textinput.New() | ||||
| 
 | ||||
| 	model := uiModel{ | ||||
| 		table:        tbl, | ||||
| 		tableService: tableService, | ||||
| 		tableName:    tableName, | ||||
| 		msgPublisher: msgPublisher, | ||||
| 		message:      "Press s to scan", | ||||
| 		table:     tbl, | ||||
| 		message:   "Press s to scan", | ||||
| 		textInput: textInput, | ||||
| 
 | ||||
| 		dispatcher:           dispatcher, | ||||
| 		tableReadController:  tableReadController, | ||||
| 		tableWriteController: tableWriteController, | ||||
| 	} | ||||
| 
 | ||||
| 	return model | ||||
|  | @ -65,16 +82,19 @@ func (m *uiModel) updateTable() { | |||
| 	m.table = newTbl | ||||
| } | ||||
| 
 | ||||
| func (m *uiModel) selectedItem() (itemTableRow, bool) { | ||||
| 	if m.ready && m.resultSet != nil && len(m.resultSet.Items) > 0 { | ||||
| 		selectedItem, ok := m.table.SelectedRow().(itemTableRow) | ||||
| 		if ok { | ||||
| 			return selectedItem, true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return itemTableRow{}, false | ||||
| } | ||||
| 
 | ||||
| func (m *uiModel) updateViewportToSelectedMessage() { | ||||
| 	if !m.ready { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if m.resultSet == nil || len(m.resultSet.Items) == 0 { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	selectedItem, ok := m.table.SelectedRow().(itemTableRow) | ||||
| 	selectedItem, ok := m.selectedItem() | ||||
| 	if !ok { | ||||
| 		m.viewport.SetContent("(no row selected)") | ||||
| 		return | ||||
|  | @ -83,17 +103,15 @@ func (m *uiModel) updateViewportToSelectedMessage() { | |||
| 	viewportContent := &strings.Builder{} | ||||
| 	tabWriter := tabwriter.NewWriter(viewportContent, 0, 1, 1, ' ', 0) | ||||
| 	for _, colName := range selectedItem.resultSet.Columns { | ||||
| 		fmt.Fprintf(tabWriter, "%v\t", colName) | ||||
| 
 | ||||
| 		switch colVal := selectedItem.item[colName].(type) { | ||||
| 		case nil: | ||||
| 			fmt.Fprintln(tabWriter, "(nil)") | ||||
| 			break | ||||
| 		case *types.AttributeValueMemberS: | ||||
| 			fmt.Fprintln(tabWriter, colVal.Value) | ||||
| 			fmt.Fprintf(tabWriter, "%v\tS\t%s\n", colName, colVal.Value) | ||||
| 		case *types.AttributeValueMemberN: | ||||
| 			fmt.Fprintln(tabWriter, colVal.Value) | ||||
| 			fmt.Fprintf(tabWriter, "%v\tN\t%s\n", colName, colVal.Value) | ||||
| 		default: | ||||
| 			fmt.Fprintln(tabWriter, "(other)") | ||||
| 			fmt.Fprintf(tabWriter, "%v\t?\t%s\n", colName, "(other)") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -102,26 +120,38 @@ func (m *uiModel) updateViewportToSelectedMessage() { | |||
| } | ||||
| 
 | ||||
| func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||||
| 	var textInputCommands tea.Cmd | ||||
| 
 | ||||
| 	switch msg := msg.(type) { | ||||
| 	case setStatusMessage: | ||||
| 		m.message = "" | ||||
| 	case errorRaised: | ||||
| 		m.message = "Error: " + msg.Error() | ||||
| 	case newResultSet: | ||||
| 
 | ||||
| 	// Local events
 | ||||
| 	case controllers.NewResultSet: | ||||
| 		m.resultSet = msg.ResultSet | ||||
| 		m.updateTable() | ||||
| 		m.updateViewportToSelectedMessage() | ||||
| 
 | ||||
| 	// Shared events
 | ||||
| 	case events.Error: | ||||
| 		m.message = "Error: " + msg.Error() | ||||
| 	case events.Message: | ||||
| 		m.message = string(msg) | ||||
| 	case events.PromptForInput: | ||||
| 		m.textInput.Focus() | ||||
| 		m.textInput.SetValue("") | ||||
| 		m.pendingInput = &msg | ||||
| 
 | ||||
| 	// Tea events
 | ||||
| 	case tea.WindowSizeMsg: | ||||
| 		footerHeight := lipgloss.Height(m.footerView()) | ||||
| 		fixedViewsHeight := lipgloss.Height(m.headerView()) + lipgloss.Height(m.splitterView()) + lipgloss.Height(m.footerView()) | ||||
| 		tableHeight := msg.Height / 2 | ||||
| 
 | ||||
| 		if !m.ready { | ||||
| 			m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-footerHeight) | ||||
| 			m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-fixedViewsHeight) | ||||
| 			m.viewport.SetContent("(no message selected)") | ||||
| 			m.ready = true | ||||
| 		} else { | ||||
| 			m.viewport.Width = msg.Width | ||||
| 			m.viewport.Height = msg.Height - tableHeight - footerHeight | ||||
| 			m.viewport.Height = msg.Height - tableHeight - fixedViewsHeight | ||||
| 		} | ||||
| 
 | ||||
| 		m.tableWidth, m.tableHeight = msg.Width, tableHeight | ||||
|  | @ -129,15 +159,21 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 
 | ||||
| 	case tea.KeyMsg: | ||||
| 
 | ||||
| 		// If text input in focus, allow that to accept input messages
 | ||||
| 		if m.pendingInput != nil { | ||||
| 			switch msg.String() { | ||||
| 			case "ctrl+c", "esc": | ||||
| 				m.pendingInput = nil | ||||
| 			case "enter": | ||||
| 				m.dispatcher.Start(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone) | ||||
| 				m.pendingInput = nil | ||||
| 			default: | ||||
| 				m.textInput, textInputCommands = m.textInput.Update(msg) | ||||
| 			} | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		switch msg.String() { | ||||
| 		case "s": | ||||
| 			m.startOperation("Scanning...", func(ctx context.Context) (tea.Msg, error) { | ||||
| 				resultSet, err := m.tableService.Scan(ctx, m.tableName) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				return newResultSet{resultSet}, nil | ||||
| 			}) | ||||
| 		case "ctrl+c", "q": | ||||
| 			return m, tea.Quit | ||||
| 		case "up", "i": | ||||
|  | @ -146,7 +182,17 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 		case "down", "k": | ||||
| 			m.table.GoDown() | ||||
| 			m.updateViewportToSelectedMessage() | ||||
| 
 | ||||
| 		// TODO: these should be moved somewhere else
 | ||||
| 		case "s": | ||||
| 			m.dispatcher.Start(context.Background(), m.tableReadController.Scan()) | ||||
| 		case "D": | ||||
| 			if selectedItem, ok := m.selectedItem(); ok { | ||||
| 				m.dispatcher.Start(context.Background(), m.tableWriteController.Delete(selectedItem.item)) | ||||
| 			} | ||||
| 		} | ||||
| 	default: | ||||
| 		m.textInput, textInputCommands = m.textInput.Update(msg) | ||||
| 	} | ||||
| 
 | ||||
| 	updatedTable, tableMsgs := m.table.Update(msg) | ||||
|  | @ -155,21 +201,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 	m.table = updatedTable | ||||
| 	m.viewport = updatedViewport | ||||
| 
 | ||||
| 	return m, tea.Batch(tableMsgs, viewportMsgs) | ||||
| } | ||||
| 
 | ||||
| // TODO: this should probably be a separate service
 | ||||
| func (m *uiModel) startOperation(msg string, op func(ctx context.Context) (tea.Msg, error)) { | ||||
| 	m.message = msg | ||||
| 	go func() { | ||||
| 		resMsg, err := op(context.Background()) | ||||
| 		if err != nil { | ||||
| 			m.msgPublisher.Send(errorRaised(err)) | ||||
| 		} else if resMsg != nil { | ||||
| 			m.msgPublisher.Send(resMsg) | ||||
| 		} | ||||
| 		m.msgPublisher.Send(setStatusMessage("")) | ||||
| 	}() | ||||
| 	return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs) | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) View() string { | ||||
|  | @ -177,9 +209,35 @@ func (m uiModel) View() string { | |||
| 		return "Initializing" | ||||
| 	} | ||||
| 
 | ||||
| 	log.Println("Returning full view") | ||||
| 	return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.viewport.View(), m.footerView()) | ||||
| 	//return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.footerView())
 | ||||
| 	if m.pendingInput != nil { | ||||
| 		return lipgloss.JoinVertical(lipgloss.Top, | ||||
| 			m.headerView(), | ||||
| 			m.table.View(), | ||||
| 			m.splitterView(), | ||||
| 			m.viewport.View(), | ||||
| 			m.textInput.View(), | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	return lipgloss.JoinVertical(lipgloss.Top, | ||||
| 		m.headerView(), | ||||
| 		m.table.View(), | ||||
| 		m.splitterView(), | ||||
| 		m.viewport.View(), | ||||
| 		m.footerView(), | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) headerView() string { | ||||
| 	title := headerStyle.Render("Table: XXX") | ||||
| 	line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))) | ||||
| 	return lipgloss.JoinHorizontal(lipgloss.Left, title, line) | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) splitterView() string { | ||||
| 	title := headerStyle.Render("Item") | ||||
| 	line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))) | ||||
| 	return lipgloss.JoinHorizontal(lipgloss.Left, title, line) | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) footerView() string { | ||||
|  |  | |||
							
								
								
									
										39
									
								
								internal/sqs-browse/controllers/forward.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/sqs-browse/controllers/forward.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| package controllers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/models" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/services/messages" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
| 
 | ||||
| type MessageSendingController struct { | ||||
| 	messageService *messages.Service | ||||
| 	targetQueue    string | ||||
| } | ||||
| 
 | ||||
| func NewMessageSendingController(messageService *messages.Service, targetQueue string) *MessageSendingController { | ||||
| 	return &MessageSendingController{ | ||||
| 		messageService: messageService, | ||||
| 		targetQueue:    targetQueue, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (msh *MessageSendingController) ForwardMessage(message models.Message) uimodels.Operation { | ||||
| 	return uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 		uiCtx := uimodels.Ctx(ctx) | ||||
| 
 | ||||
| 		if msh.targetQueue == "" { | ||||
| 			return errors.New("target queue not set") | ||||
| 		} | ||||
| 
 | ||||
| 		messageId, err := msh.messageService.SendTo(ctx, message, msh.targetQueue) | ||||
| 		if err != nil { | ||||
| 			return errors.Wrapf(err, "cannot send message to %v", msh.targetQueue) | ||||
| 		} | ||||
| 
 | ||||
| 		uiCtx.Message("Message sent to " + msh.targetQueue + ", id = " + messageId) | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
|  | @ -19,6 +19,20 @@ func NewProvider(client *sqs.Client) *Provider { | |||
| 	return &Provider{client: client} | ||||
| } | ||||
| 
 | ||||
| func (p *Provider) SendMessage(ctx context.Context, msg models.Message, queue string) (string, error) { | ||||
| 	// TEMP :: queue URL
 | ||||
| 
 | ||||
| 	out, err := p.client.SendMessage(ctx, &sqs.SendMessageInput{ | ||||
| 		QueueUrl: aws.String(queue), | ||||
| 		MessageBody: aws.String(msg.Data), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return "", errors.Wrapf(err, "unable to send message to %v", queue) | ||||
| 	} | ||||
| 
 | ||||
| 	return aws.ToString(out.MessageId), nil | ||||
| } | ||||
| 
 | ||||
| func (p *Provider) PollForNewMessages(ctx context.Context, queue string) ([]*models.Message, error) { | ||||
| 	out, err := p.client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{ | ||||
| 		QueueUrl:            aws.String(queue), | ||||
|  |  | |||
|  | @ -6,5 +6,5 @@ import ( | |||
| ) | ||||
| 
 | ||||
| type MessageSender interface { | ||||
| 	SendMessage(ctx context.Context, msg models.Message, queue string) error | ||||
| 	SendMessage(ctx context.Context, msg models.Message, queue string) (string, error) | ||||
| } | ||||
|  | @ -10,10 +10,16 @@ type Service struct { | |||
| 	messageSender MessageSender | ||||
| } | ||||
| 
 | ||||
| func NewService() *Service { | ||||
| 	return &Service{} | ||||
| func NewService(messageSender MessageSender) *Service { | ||||
| 	return &Service{ | ||||
| 		messageSender: messageSender, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *Service) SendTo(ctx context.Context, msg models.Message, destQueue string) error { | ||||
| 	return errors.Wrapf(s.messageSender.SendMessage(ctx, msg, destQueue), "cannot send message to %v", destQueue) | ||||
| func (s *Service) SendTo(ctx context.Context, msg models.Message, destQueue string) (string, error) { | ||||
| 	messageId, err := s.messageSender.SendMessage(ctx, msg, destQueue) | ||||
| 	if err != nil { | ||||
| 		return "", errors.Wrapf(err, "cannot send message to %v", destQueue) | ||||
| 	} | ||||
| 	return messageId, nil | ||||
| } | ||||
|  |  | |||
|  | @ -1,14 +1,28 @@ | |||
| package ui | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	table "github.com/calyptia/go-bubble-table" | ||||
| 	"github.com/charmbracelet/bubbles/textinput" | ||||
| 	"github.com/charmbracelet/bubbles/viewport" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/charmbracelet/lipgloss" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/dispatcher" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/events" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/controllers" | ||||
| 	"github.com/lmika/awstools/internal/sqs-browse/models" | ||||
| 	"log" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	headerStyle = lipgloss.NewStyle(). | ||||
| 		Bold(true). | ||||
| 		Foreground(lipgloss.Color("#ffffff")). | ||||
| 		Background(lipgloss.Color("#eac610")) | ||||
| ) | ||||
| 
 | ||||
| type uiModel struct { | ||||
| 	table    table.Model | ||||
| 	viewport viewport.Model | ||||
|  | @ -16,17 +30,28 @@ type uiModel struct { | |||
| 	ready     bool | ||||
| 	tableRows []table.Row | ||||
| 	message   string | ||||
| 
 | ||||
| 	pendingInput *events.PromptForInput | ||||
| 	textInput    textinput.Model | ||||
| 
 | ||||
| 	dispatcher         *dispatcher.Dispatcher | ||||
| 	msgSendingHandlers *controllers.MessageSendingController | ||||
| } | ||||
| 
 | ||||
| func NewModel() tea.Model { | ||||
| func NewModel(dispatcher *dispatcher.Dispatcher, msgSendingHandlers *controllers.MessageSendingController) tea.Model { | ||||
| 	tbl := table.New([]string{"seq", "message"}, 100, 20) | ||||
| 	rows := make([]table.Row, 0) | ||||
| 	tbl.SetRows(rows) | ||||
| 
 | ||||
| 	textInput := textinput.New() | ||||
| 
 | ||||
| 	model := uiModel{ | ||||
| 		table:     tbl, | ||||
| 		tableRows: rows, | ||||
| 		message:   "", | ||||
| 		table:      tbl, | ||||
| 		tableRows:  rows, | ||||
| 		message:    "", | ||||
| 		textInput:  textInput, | ||||
| 		msgSendingHandlers: msgSendingHandlers, | ||||
| 		dispatcher: dispatcher, | ||||
| 	} | ||||
| 
 | ||||
| 	return model | ||||
|  | @ -37,23 +62,37 @@ func (m uiModel) Init() tea.Cmd { | |||
| } | ||||
| 
 | ||||
| func (m *uiModel) updateViewportToSelectedMessage() { | ||||
| 	if !m.ready { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if len(m.tableRows) == 0 { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if message, ok := m.table.SelectedRow().(messageTableRow); ok { | ||||
| 	if message, ok := m.selectedMessage(); ok { | ||||
| 		m.viewport.SetContent(message.Data) | ||||
| 	} else { | ||||
| 		m.viewport.SetContent("(no message selected)") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) selectedMessage() (models.Message, bool) { | ||||
| 	if m.ready && len(m.tableRows) > 0 { | ||||
| 		if message, ok := m.table.SelectedRow().(messageTableRow); ok { | ||||
| 			return models.Message(message), true | ||||
| 		} | ||||
| 	} | ||||
| 	return models.Message{}, false | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||||
| 	var textInputCommands tea.Cmd | ||||
| 
 | ||||
| 	switch msg := msg.(type) { | ||||
| 	// Shared messages
 | ||||
| 	case events.Error: | ||||
| 		m.message = "Error: " + msg.Error() | ||||
| 	case events.Message: | ||||
| 		m.message = string(msg) | ||||
| 	case events.PromptForInput: | ||||
| 		m.textInput.Focus() | ||||
| 		m.textInput.SetValue("") | ||||
| 		m.pendingInput = &msg | ||||
| 
 | ||||
| 	// Local messages
 | ||||
| 	case NewMessagesEvent: | ||||
| 		for _, newMsg := range msg { | ||||
| 			m.tableRows = append(m.tableRows, messageTableRow(*newMsg)) | ||||
|  | @ -62,13 +101,13 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 		m.updateViewportToSelectedMessage() | ||||
| 
 | ||||
| 	case tea.WindowSizeMsg: | ||||
| 		footerHeight := lipgloss.Height(m.footerView()) | ||||
| 		fixedViewsHeight := lipgloss.Height(m.headerView()) + lipgloss.Height(m.splitterView()) + lipgloss.Height(m.footerView()) | ||||
| 
 | ||||
| 		if !m.ready { | ||||
| 			tableHeight := msg.Height / 2 | ||||
| 
 | ||||
| 			m.table.SetSize(msg.Width, tableHeight) | ||||
| 			m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-footerHeight) | ||||
| 			m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-fixedViewsHeight) | ||||
| 			m.viewport.SetContent("(no message selected)") | ||||
| 			m.ready = true | ||||
| 			log.Println("Viewport is now ready") | ||||
|  | @ -77,12 +116,29 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 
 | ||||
| 			m.table.SetSize(msg.Width, tableHeight) | ||||
| 			m.viewport.Width = msg.Width | ||||
| 			m.viewport.Height = msg.Height - tableHeight - footerHeight | ||||
| 			//m.viewport.YPosition = tableHeight
 | ||||
| 			m.viewport.Height = msg.Height - tableHeight - fixedViewsHeight | ||||
| 		} | ||||
| 
 | ||||
| 		m.textInput.Width = msg.Width | ||||
| 
 | ||||
| 		m.textInput, textInputCommands = m.textInput.Update(msg) | ||||
| 	case tea.KeyMsg: | ||||
| 
 | ||||
| 		// If text input in focus, allow that to accept input messages
 | ||||
| 		if m.pendingInput != nil { | ||||
| 			switch msg.String() { | ||||
| 			case "ctrl+c", "esc": | ||||
| 				m.pendingInput = nil | ||||
| 			case "enter": | ||||
| 				m.dispatcher.Start(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone) | ||||
| 				m.pendingInput = nil | ||||
| 			default: | ||||
| 				m.textInput, textInputCommands = m.textInput.Update(msg) | ||||
| 			} | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		// Normal focus
 | ||||
| 		switch msg.String() { | ||||
| 
 | ||||
| 		case "ctrl+c", "q": | ||||
|  | @ -93,7 +149,15 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 		case "down", "k": | ||||
| 			m.table.GoDown() | ||||
| 			m.updateViewportToSelectedMessage() | ||||
| 
 | ||||
| 		// TODO: these should be moved somewhere else
 | ||||
| 		case "f": | ||||
| 			if selectedMessage, ok := m.selectedMessage(); ok { | ||||
| 				m.dispatcher.Start(context.Background(), m.msgSendingHandlers.ForwardMessage(selectedMessage)) | ||||
| 			} | ||||
| 		} | ||||
| 	default: | ||||
| 		m.textInput, textInputCommands = m.textInput.Update(msg) | ||||
| 	} | ||||
| 
 | ||||
| 	updatedTable, tableMsgs := m.table.Update(msg) | ||||
|  | @ -102,7 +166,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 	m.table = updatedTable | ||||
| 	m.viewport = updatedViewport | ||||
| 
 | ||||
| 	return m, tea.Batch(tableMsgs, viewportMsgs) | ||||
| 	return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs) | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) View() string { | ||||
|  | @ -110,9 +174,35 @@ func (m uiModel) View() string { | |||
| 		return "Initializing" | ||||
| 	} | ||||
| 
 | ||||
| 	log.Println("Returning full view") | ||||
| 	return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.viewport.View(), m.footerView()) | ||||
| 	//return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.footerView())
 | ||||
| 	if m.pendingInput != nil { | ||||
| 		return lipgloss.JoinVertical(lipgloss.Top, | ||||
| 			m.headerView(), | ||||
| 			m.table.View(), | ||||
| 			m.splitterView(), | ||||
| 			m.viewport.View(), | ||||
| 			m.textInput.View(), | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	return lipgloss.JoinVertical(lipgloss.Top, | ||||
| 		m.headerView(), | ||||
| 		m.table.View(), | ||||
| 		m.splitterView(), | ||||
| 		m.viewport.View(), | ||||
| 		m.footerView(), | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) headerView() string { | ||||
| 	title := headerStyle.Render("Queue: XXX") | ||||
| 	line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))) | ||||
| 	return lipgloss.JoinHorizontal(lipgloss.Left, title, line) | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) splitterView() string { | ||||
| 	title := headerStyle.Render("Message") | ||||
| 	line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))) | ||||
| 	return lipgloss.JoinHorizontal(lipgloss.Left, title, line) | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) footerView() string { | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue