diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 4a06960..f08ea81 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -7,7 +7,9 @@ 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/commandctrl" "github.com/lmika/awstools/internal/common/ui/dispatcher" + "github.com/lmika/awstools/internal/common/ui/uimodels" "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" @@ -45,7 +47,13 @@ func main() { tableReadController := controllers.NewTableReadController(tableService, *flagTable) tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, *flagTable) - uiModel := ui.NewModel(uiDispatcher, tableReadController, tableWriteController) + commandController := commandctrl.NewCommandController(map[string]uimodels.Operation{ + "scan": tableReadController.Scan(), + "rw": tableWriteController.ToggleReadWrite(), + "dup": tableWriteController.Duplicate(), + }) + + uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController) p := tea.NewProgram(uiModel, tea.WithAltScreen()) loopback.program = p diff --git a/go.mod b/go.mod index 1a3679c..fa1f6cb 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( ) require ( + github.com/alecthomas/participle/v2 v2.0.0-alpha7 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 // indirect @@ -36,6 +37,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect + github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index c52fe47..eb66876 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs= +github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c= +github.com/alecthomas/participle/v2 v2.0.0-alpha7/go.mod h1:NumScqsC42o9x+dGj8/YqsIfhrIQjFEOFovxotbBirA= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= 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= @@ -75,6 +79,8 @@ github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e h1:0QkUe2ejnT/i+xbgGy github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e/go.mod h1:qtkBmNC9OfD0STtOR9sF55pQchjIfNlC3gzm4n8CrqM= github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 h1:mwl/exYV/WkBMeShqK7q+B2w2r+b0vP1TSA7clBn9kI= github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890/go.mod h1:FH6OJSvYcJ9xY8CGs9yGgR89kMCK1UimuUQ6kE5YuJQ= +github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18= +github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe/go.mod h1:qpdOkLougV5Yry4Px9f1w1pNMavcr6Z67VW5Ro+vW5I= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= @@ -106,6 +112,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -121,6 +128,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go new file mode 100644 index 0000000..ef26530 --- /dev/null +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -0,0 +1,48 @@ +package commandctrl + +import ( + "context" + "github.com/lmika/awstools/internal/common/ui/events" + "github.com/lmika/awstools/internal/common/ui/uimodels" + "github.com/lmika/shellwords" + "github.com/pkg/errors" + "strings" +) + +type CommandController struct { + commands map[string]uimodels.Operation +} + +func NewCommandController(commands map[string]uimodels.Operation) *CommandController { + return &CommandController{ + commands: commands, + } +} + +func (c *CommandController) Prompt() uimodels.Operation { + return uimodels.OperationFn(func(ctx context.Context) error { + uiCtx := uimodels.Ctx(ctx) + uiCtx.Send(events.PromptForInput{ + Prompt: ":", + OnDone: c.Execute(), + }) + return nil + }) +} + +func (c *CommandController) Execute() uimodels.Operation { + return uimodels.OperationFn(func(ctx context.Context) error { + input := strings.TrimSpace(uimodels.PromptValue(ctx)) + if input == "" { + return nil + } + + tokens := shellwords.Split(input) + command, ok := c.commands[tokens[0]] + if !ok { + return errors.New("no such command: " + tokens[0]) + } + + return command.Execute(WithCommandArgs(ctx, tokens[1:])) + }) +} \ No newline at end of file diff --git a/internal/common/ui/commandctrl/commandctrl_test.go b/internal/common/ui/commandctrl/commandctrl_test.go new file mode 100644 index 0000000..85ef20a --- /dev/null +++ b/internal/common/ui/commandctrl/commandctrl_test.go @@ -0,0 +1,25 @@ +package commandctrl_test + +import ( + "context" + "github.com/lmika/awstools/internal/common/ui/commandctrl" + "github.com/lmika/awstools/internal/common/ui/events" + "github.com/lmika/awstools/test/testuictx" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandController_Prompt(t *testing.T) { + t.Run("prompt user for a command", func(t *testing.T) { + cmd := commandctrl.NewCommandController() + + ctx, uiCtx := testuictx.New(context.Background()) + err := cmd.Prompt().Execute(ctx) + + assert.NoError(t, err) + + promptMsg, ok := uiCtx.Messages[0].(events.PromptForInput) + assert.True(t, ok) + assert.Equal(t, ":", promptMsg.Prompt) + }) +} diff --git a/internal/common/ui/commandctrl/context.go b/internal/common/ui/commandctrl/context.go new file mode 100644 index 0000000..6728126 --- /dev/null +++ b/internal/common/ui/commandctrl/context.go @@ -0,0 +1,15 @@ +package commandctrl + +import "context" + +type commandArgContextKeyType struct {} +var commandArgContextKey = commandArgContextKeyType{} + +func WithCommandArgs(ctx context.Context, args []string) context.Context { + return context.WithValue(ctx, commandArgContextKey, args) +} + +func CommandArgs(ctx context.Context) []string { + args, _ := ctx.Value(commandArgContextKey).([]string) + return args +} \ No newline at end of file diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index c67cb23..9634f05 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -3,6 +3,7 @@ package controllers import ( "context" "github.com/lmika/awstools/internal/common/ui/uimodels" + "github.com/lmika/awstools/internal/dynamo-browse/models/modexpr" "github.com/lmika/awstools/internal/dynamo-browse/services/tables" "github.com/pkg/errors" ) @@ -21,12 +22,67 @@ func NewTableWriteController(tableService *tables.Service, tableReadControllers } } -func (c *TableWriteController) EnableReadWrite() uimodels.Operation { +func (c *TableWriteController) ToggleReadWrite() uimodels.Operation { return uimodels.OperationFn(func(ctx context.Context) error { uiCtx := uimodels.Ctx(ctx) - uiCtx.Send(SetReadWrite{NewValue: true}) - uiCtx.Message("read/write mode enabled") + state := CurrentState(ctx) + if state.InReadWriteMode { + uiCtx.Send(SetReadWrite{NewValue: false}) + uiCtx.Message("read/write mode disabled") + } else { + uiCtx.Send(SetReadWrite{NewValue: true}) + uiCtx.Message("read/write mode enabled") + } + + return nil + }) +} + +func (c *TableWriteController) Duplicate() uimodels.Operation { + return uimodels.OperationFn(func(ctx context.Context) error { + uiCtx := uimodels.Ctx(ctx) + state := CurrentState(ctx) + + if state.SelectedItem == nil { + return errors.New("no selected item") + } else if !state.InReadWriteMode { + return errors.New("not in read/write mode") + } + + uiCtx.Input("Dup: ", uimodels.OperationFn(func(ctx context.Context) error { + modExpr, err := modexpr.Parse(uimodels.PromptValue(ctx)) + if err != nil { + return err + } + + newItem, err := modExpr.Patch(state.SelectedItem) + if err != nil { + return err + } + + // TODO: preview new item + + uiCtx := uimodels.Ctx(ctx) + uiCtx.Input("Put item? ", uimodels.OperationFn(func(ctx context.Context) error { + if uimodels.PromptValue(ctx) != "y" { + return errors.New("operation aborted") + } + + // Delete the item + if err := c.tableService.Put(ctx, c.tableName, newItem); err != nil { + return err + } + + // Rescan to get updated items + if err := c.tableReadControllers.doScan(ctx, true); err != nil { + return err + } + + return nil + })) + return nil + })) return nil }) } diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index ed549f7..c8f8722 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -13,21 +13,33 @@ import ( "testing" ) -func TestTableWriteController_EnableReadWrite(t *testing.T) { +func TestTableWriteController_ToggleReadWrite(t *testing.T) { twc, _, closeFn := setupController(t) t.Cleanup(closeFn) - t.Run("should send event enabling read write", func(t *testing.T) { + t.Run("should enabling read write if disabled", func(t *testing.T) { ctx, uiCtx := testuictx.New(context.Background()) ctx = controllers.ContextWithState(ctx, controllers.State{ InReadWriteMode: false, }) - err := twc.EnableReadWrite().Execute(ctx) + err := twc.ToggleReadWrite().Execute(ctx) assert.NoError(t, err) assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: true}) }) + + t.Run("should disable read write if enabled", func(t *testing.T) { + ctx, uiCtx := testuictx.New(context.Background()) + ctx = controllers.ContextWithState(ctx, controllers.State{ + InReadWriteMode: true, + }) + + err := twc.ToggleReadWrite().Execute(ctx) + assert.NoError(t, err) + + assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: false}) + }) } func TestTableWriteController_Delete(t *testing.T) { diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go index a3ad1bd..9afcfeb 100644 --- a/internal/dynamo-browse/models/models.go +++ b/internal/dynamo-browse/models/models.go @@ -9,3 +9,15 @@ type ResultSet struct { } type Item map[string]types.AttributeValue + +// Clone creates a clone of the current item +func (i Item) Clone() Item { + newItem := Item{} + + // TODO: should be a deep clone? + for k, v := range i { + newItem[k] = v + } + + return newItem +} diff --git a/internal/dynamo-browse/models/modexpr/ast.go b/internal/dynamo-browse/models/modexpr/ast.go new file mode 100644 index 0000000..70a2845 --- /dev/null +++ b/internal/dynamo-browse/models/modexpr/ast.go @@ -0,0 +1,39 @@ +package modexpr + +import ( + "github.com/alecthomas/participle/v2" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/pkg/errors" + "strconv" +) + +type astExpr struct { + Attributes []*astAttribute `parser:"@@ (',' @@)*"` +} + +type astAttribute struct { + Name string `parser:"@Ident '='"` + Value string `parser:"@String"` +} + +func (a astAttribute) dynamoValue() (types.AttributeValue, error) { + // TODO: should be based on type + s, err := strconv.Unquote(a.Value) + if err != nil { + return nil, errors.Wrap(err, "cannot unquote string") + } + return &types.AttributeValueMemberS{Value: s}, nil +} + + +var parser = participle.MustBuild(&astExpr{}) + +func Parse(expr string) (*ModExpr, error) { + var ast astExpr + + if err := parser.ParseString("expr", expr, &ast); err != nil { + return nil, errors.Wrapf(err, "cannot parse expression: '%v'", expr) + } + + return &ModExpr{ast: &ast}, nil +} \ No newline at end of file diff --git a/internal/dynamo-browse/models/modexpr/expr.go b/internal/dynamo-browse/models/modexpr/expr.go new file mode 100644 index 0000000..2f381b8 --- /dev/null +++ b/internal/dynamo-browse/models/modexpr/expr.go @@ -0,0 +1,22 @@ +package modexpr + +import "github.com/lmika/awstools/internal/dynamo-browse/models" + +type ModExpr struct { + ast *astExpr +} + +func (me *ModExpr) Patch(item models.Item) (models.Item, error) { + newItem := item.Clone() + + for _, attribute := range me.ast.Attributes { + var err error + name := attribute.Name + newItem[name], err = attribute.dynamoValue() + if err != nil { + return nil, err + } + } + + return newItem, nil +} diff --git a/internal/dynamo-browse/models/modexpr/expr_test.go b/internal/dynamo-browse/models/modexpr/expr_test.go new file mode 100644 index 0000000..b6213fa --- /dev/null +++ b/internal/dynamo-browse/models/modexpr/expr_test.go @@ -0,0 +1,39 @@ +package modexpr_test + +import ( + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/awstools/internal/dynamo-browse/models" + "github.com/lmika/awstools/internal/dynamo-browse/models/modexpr" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestModExpr_Patch(t *testing.T) { + t.Run("patch with new attributes", func(t *testing.T) { + modExpr, err := modexpr.Parse(`alpha="new value", beta="another new value"`) + assert.NoError(t, err) + + oldItem := models.Item{} + newItem, err := modExpr.Patch(oldItem) + assert.NoError(t, err) + + assert.Equal(t, "new value", newItem["alpha"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another new value", newItem["beta"].(*types.AttributeValueMemberS).Value) + }) + + t.Run("patch with existing attributes", func(t *testing.T) { + modExpr, err := modexpr.Parse(`alpha="new value", beta="another new value"`) + assert.NoError(t, err) + + oldItem := models.Item{ + "old": &types.AttributeValueMemberS{Value: "before"}, + "beta": &types.AttributeValueMemberS{Value: "before beta"}, + } + newItem, err := modExpr.Patch(oldItem) + assert.NoError(t, err) + + assert.Equal(t, "before", newItem["old"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "new value", newItem["alpha"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another new value", newItem["beta"].(*types.AttributeValueMemberS).Value) + }) +} diff --git a/internal/dynamo-browse/providers/dynamo/provider.go b/internal/dynamo-browse/providers/dynamo/provider.go index f1910c1..bdb110a 100644 --- a/internal/dynamo-browse/providers/dynamo/provider.go +++ b/internal/dynamo-browse/providers/dynamo/provider.go @@ -13,6 +13,17 @@ type Provider struct { client *dynamodb.Client } +func (p *Provider) PutItem(ctx context.Context, name string, item models.Item) error { + _, err := p.client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(name), + Item: item, + }) + if err != nil { + return errors.Wrapf(err, "cannot execute put on table %v", name) + } + return nil +} + func NewProvider(client *dynamodb.Client) *Provider { return &Provider{client: client} } diff --git a/internal/dynamo-browse/services/tables/iface.go b/internal/dynamo-browse/services/tables/iface.go index 1e46d27..4c43779 100644 --- a/internal/dynamo-browse/services/tables/iface.go +++ b/internal/dynamo-browse/services/tables/iface.go @@ -9,4 +9,5 @@ import ( type TableProvider interface { ScanItems(ctx context.Context, tableName string) ([]models.Item, error) DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error + PutItem(ctx context.Context, name string, item models.Item) error } diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index dbcc3c5..bed1d02 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -60,6 +60,10 @@ func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, er }, nil } +func (s *Service) Put(ctx context.Context, tableName string, item models.Item) error { + return s.provider.PutItem(ctx, tableName, item) +} + 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{ diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 90ad0da..65754ab 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/lmika/awstools/internal/common/ui/commandctrl" "github.com/lmika/awstools/internal/common/ui/dispatcher" "github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/common/ui/uimodels" @@ -19,13 +20,13 @@ import ( var ( activeHeaderStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("#ffffff")). - Background(lipgloss.Color("#4479ff")) + Bold(true). + Foreground(lipgloss.Color("#ffffff")). + Background(lipgloss.Color("#4479ff")) inactiveHeaderStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#000000")). - Background(lipgloss.Color("#d1d1d1")) + Foreground(lipgloss.Color("#000000")). + Background(lipgloss.Color("#d1d1d1")) ) type uiModel struct { @@ -34,20 +35,21 @@ type uiModel struct { tableWidth, tableHeight int - ready bool + ready bool //resultSet *models.ResultSet - state controllers.State - message string + state controllers.State + message string pendingInput *events.PromptForInput textInput textinput.Model dispatcher *dispatcher.Dispatcher + commandController *commandctrl.CommandController tableReadController *controllers.TableReadController tableWriteController *controllers.TableWriteController } -func NewModel(dispatcher *dispatcher.Dispatcher, tableReadController *controllers.TableReadController, tableWriteController *controllers.TableWriteController) tea.Model { +func NewModel(dispatcher *dispatcher.Dispatcher, commandController *commandctrl.CommandController, tableReadController *controllers.TableReadController, tableWriteController *controllers.TableWriteController) tea.Model { tbl := table.New([]string{"pk", "sk"}, 100, 20) rows := make([]table.Row, 0) tbl.SetRows(rows) @@ -60,6 +62,7 @@ func NewModel(dispatcher *dispatcher.Dispatcher, tableReadController *controller textInput: textInput, dispatcher: dispatcher, + commandController: commandController, tableReadController: tableReadController, tableWriteController: tableWriteController, } @@ -174,7 +177,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "ctrl+c", "esc": m.pendingInput = nil case "enter": - m.dispatcher.Start(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone) + m.invokeOperation(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone) m.pendingInput = nil default: m.textInput, textInputCommands = m.textInput.Update(msg) @@ -193,12 +196,12 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.updateViewportToSelectedMessage() // TODO: these should be moved somewhere else + case ":": + m.invokeOperation(context.Background(), m.commandController.Prompt()) case "s": - m.invokeOperation(m.tableReadController.Scan()) + m.invokeOperation(context.Background(), m.tableReadController.Scan()) case "D": - m.invokeOperation(m.tableWriteController.Delete()) - case "w": - m.invokeOperation(m.tableWriteController.EnableReadWrite()) + m.invokeOperation(context.Background(), m.tableWriteController.Delete()) } default: m.textInput, textInputCommands = m.textInput.Update(msg) @@ -213,13 +216,13 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs) } -func (m uiModel) invokeOperation(op uimodels.Operation) { +func (m uiModel) invokeOperation(ctx context.Context, op uimodels.Operation) { state := m.state if selectedItem, ok := m.selectedItem(); ok { state.SelectedItem = selectedItem.item } - ctx := controllers.ContextWithState(context.Background(), state) + ctx = controllers.ContextWithState(ctx, state) m.dispatcher.Start(ctx, op) } diff --git a/internal/sqs-browse/ui/model.go b/internal/sqs-browse/ui/model.go index 7f42f3e..0549222 100644 --- a/internal/sqs-browse/ui/model.go +++ b/internal/sqs-browse/ui/model.go @@ -1,7 +1,9 @@ package ui import ( + "bytes" "context" + "encoding/json" table "github.com/calyptia/go-bubble-table" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" @@ -67,7 +69,13 @@ func (m uiModel) Init() tea.Cmd { func (m *uiModel) updateViewportToSelectedMessage() { if message, ok := m.selectedMessage(); ok { - m.viewport.SetContent(message.Data) + // TODO: not all messages are JSON + formattedJson := new(bytes.Buffer) + if err := json.Indent(formattedJson, []byte(message.Data), "", " "); err == nil { + m.viewport.SetContent(formattedJson.String()) + } else { + m.viewport.SetContent(message.Data) + } } else { m.viewport.SetContent("(no message selected)") } @@ -204,8 +212,8 @@ func (m uiModel) headerView() string { } func (m uiModel) splitterView() string { - title := activeHeaderStyle.Render("Message") - line := activeHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))) + title := inactiveHeaderStyle.Render("Message") + line := inactiveHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))) return lipgloss.JoinHorizontal(lipgloss.Left, title, line) }