From 5d1f4c78f4ab789caa7064febe700f03b0cf412a Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 23 Mar 2022 08:58:42 +1100 Subject: [PATCH] sqs-browse: new tool Started working on a new tool to poll and browse an SQS queue. This is built using a TUI framework --- cmd/sqs-browse/main.go | 61 ++++++++++++++ go.mod | 17 ++++ go.sum | 60 ++++++++++++++ internal/sqs-browse/models/message.go | 11 +++ .../sqs-browse/providers/memstore/memstore.go | 31 +++++++ internal/sqs-browse/providers/sqs/provider.go | 63 +++++++++++++++ .../sqs-browse/services/pollmessage/iface.go | 14 ++++ .../services/pollmessage/service.go | 45 +++++++++++ internal/sqs-browse/ui/events.go | 5 ++ internal/sqs-browse/ui/model.go | 80 +++++++++++++++++++ internal/sqs-browse/ui/tblmodel.go | 26 ++++++ 11 files changed, 413 insertions(+) create mode 100644 cmd/sqs-browse/main.go create mode 100644 internal/sqs-browse/models/message.go create mode 100644 internal/sqs-browse/providers/memstore/memstore.go create mode 100644 internal/sqs-browse/providers/sqs/provider.go create mode 100644 internal/sqs-browse/services/pollmessage/iface.go create mode 100644 internal/sqs-browse/services/pollmessage/service.go create mode 100644 internal/sqs-browse/ui/events.go create mode 100644 internal/sqs-browse/ui/model.go create mode 100644 internal/sqs-browse/ui/tblmodel.go diff --git a/cmd/sqs-browse/main.go b/cmd/sqs-browse/main.go new file mode 100644 index 0000000..97c0477 --- /dev/null +++ b/cmd/sqs-browse/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "flag" + "fmt" + "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/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/pollmessage" + "github.com/lmika/awstools/internal/sqs-browse/ui" + "github.com/lmika/events" + "github.com/lmika/gopkgs/cli" + "log" + "os" +) + +func main() { + var flagQueue = flag.String("q", "", "queue to poll") + flag.Parse() + + ctx := context.Background() + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + cli.Fatalf("cannot load AWS config: %v", err) + } + sqsClient := sqs.NewFromConfig(cfg) + + bus := events.New() + + msgStore := memstore.NewStore() + msgPoller := sqsprovider.NewProvider(sqsClient) + + pollService := pollmessage.NewService(msgStore, msgPoller, *flagQueue, bus) + + uiModel := ui.NewModel() + p := tea.NewProgram(uiModel, tea.WithAltScreen()) + + bus.On("new-messages", func(m []*models.Message) { p.Send(ui.NewMessagesEvent(m)) }) + + f, err := tea.LogToFile("debug.log", "debug") + if err != nil { + fmt.Println("fatal:", err) + os.Exit(1) + } + defer f.Close() + + go func() { + if err := pollService.Poll(context.Background()); err != nil { + log.Printf("cannot start poller: %v", err) + } + }() + + if err := p.Start(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index cc2c34a..a184069 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,24 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect github.com/aws/smithy-go v1.10.0 // indirect + github.com/calyptia/go-bubble-table v0.1.0 // indirect + github.com/charmbracelet/bubbles v0.10.3 // indirect + github.com/charmbracelet/bubbletea v0.20.0 // indirect + github.com/charmbracelet/lipgloss v0.5.0 // indirect + github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect + github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e // indirect github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 // 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 + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect ) diff --git a/go.sum b/go.sum index 70d753c..cbd4468 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +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= github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo= @@ -22,6 +23,20 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk= github.com/aws/smithy-go v1.10.0 h1:gsoZQMNHnX+PaghNw4ynPsyGP7aUCqx5sY2dlPQsZ0w= github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/calyptia/go-bubble-table v0.1.0 h1:mXpaaBlrHGH4K8v5PvM8YqBFT9jlysS1YOycU2u3gEQ= +github.com/calyptia/go-bubble-table v0.1.0/go.mod h1:2nnweuFos+eEIIbgweXvZuX+ROOatsMwB3NHnX/vTC4= +github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho= +github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= +github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= +github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc= +github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM= +github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= +github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= +github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -29,12 +44,57 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoDzsfBhx0bf/1rVBXrLy8dXKRe8o= +github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e h1:0QkUe2ejnT/i+xbgGylMU1b+XnZponQKiPVNi+C/xgA= +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/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= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= +github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= +github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +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= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/sqs-browse/models/message.go b/internal/sqs-browse/models/message.go new file mode 100644 index 0000000..0d3b5e5 --- /dev/null +++ b/internal/sqs-browse/models/message.go @@ -0,0 +1,11 @@ +package models + +import "time" + +type Message struct { + ID uint64 + ExtID string + Queue string + Received time.Time + Data string +} diff --git a/internal/sqs-browse/providers/memstore/memstore.go b/internal/sqs-browse/providers/memstore/memstore.go new file mode 100644 index 0000000..23cf339 --- /dev/null +++ b/internal/sqs-browse/providers/memstore/memstore.go @@ -0,0 +1,31 @@ +package memstore + +import ( + "context" + "github.com/lmika/awstools/internal/sqs-browse/models" + "sync" +) + +type Store struct { + messages []models.Message + + mtx *sync.Mutex + currSeqNo uint64 +} + +func (s *Store) Save(ctx context.Context, msg *models.Message) error { + s.mtx.Lock() + defer s.mtx.Unlock() + + s.currSeqNo++ + msg.ID = s.currSeqNo + s.messages = append(s.messages, *msg) + return nil +} + +func NewStore() *Store { + return &Store{ + messages: make([]models.Message, 0), + mtx: new(sync.Mutex), + } +} diff --git a/internal/sqs-browse/providers/sqs/provider.go b/internal/sqs-browse/providers/sqs/provider.go new file mode 100644 index 0000000..76639c6 --- /dev/null +++ b/internal/sqs-browse/providers/sqs/provider.go @@ -0,0 +1,63 @@ +package sqs + +import ( + "context" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/aws/aws-sdk-go-v2/service/sqs/types" + "github.com/lmika/awstools/internal/sqs-browse/models" + "github.com/pkg/errors" + "log" + "time" +) + +type Provider struct { + client *sqs.Client +} + +func NewProvider(client *sqs.Client) *Provider { + return &Provider{client: client} +} + +func (p *Provider) PollForNewMessages(ctx context.Context, queue string) ([]*models.Message, error) { + out, err := p.client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{ + QueueUrl: aws.String(queue), + MaxNumberOfMessages: 10, + WaitTimeSeconds: 20, + }) + if err != nil { + return nil, errors.Wrapf(err, "unable to receive messages from queue %v", queue) + } + + if len(out.Messages) == 0 { + return nil, nil + } + + messagesToReturn := make([]*models.Message, 0, len(out.Messages)) + messagesToDelete := make([]types.DeleteMessageBatchRequestEntry, 0, len(out.Messages)) + for _, msg := range out.Messages { + newLocalMessage := &models.Message{ + Queue: queue, + ExtID: aws.ToString(msg.MessageId), + Received: time.Now(), + Data: aws.ToString(msg.Body), + } + messagesToReturn = append(messagesToReturn, newLocalMessage) + + // Pull the message from the queue + // TODO: should this be determined by the caller? + messagesToDelete = append(messagesToDelete, types.DeleteMessageBatchRequestEntry{ + Id: msg.MessageId, + ReceiptHandle: msg.ReceiptHandle, + }) + } + + if _, err := p.client.DeleteMessageBatch(ctx, &sqs.DeleteMessageBatchInput{ + QueueUrl: aws.String(queue), + Entries: messagesToDelete, + }); err != nil { + log.Printf("error deleting messages from queue: %v", err) + } + + return messagesToReturn, nil +} diff --git a/internal/sqs-browse/services/pollmessage/iface.go b/internal/sqs-browse/services/pollmessage/iface.go new file mode 100644 index 0000000..7087b28 --- /dev/null +++ b/internal/sqs-browse/services/pollmessage/iface.go @@ -0,0 +1,14 @@ +package pollmessage + +import ( + "context" + "github.com/lmika/awstools/internal/sqs-browse/models" +) + +type MessageStore interface { + Save(ctx context.Context, msg *models.Message) error +} + +type MessagePoller interface { + PollForNewMessages(ctx context.Context, queue string) ([]*models.Message, error) +} \ No newline at end of file diff --git a/internal/sqs-browse/services/pollmessage/service.go b/internal/sqs-browse/services/pollmessage/service.go new file mode 100644 index 0000000..79172b5 --- /dev/null +++ b/internal/sqs-browse/services/pollmessage/service.go @@ -0,0 +1,45 @@ +package pollmessage + +import ( + "context" + "github.com/lmika/events" + "github.com/pkg/errors" + "log" +) + +type Service struct { + store MessageStore + poller MessagePoller + queue string + bus *events.Bus +} + +func NewService(store MessageStore, poller MessagePoller, queue string, bus *events.Bus) *Service { + return &Service{ + store: store, + poller: poller, + queue: queue, + bus: bus, + } +} + +// Poll starts polling for new messages and adding them to the message store +func (s *Service) Poll(ctx context.Context) error { + for ctx.Err() == nil { + log.Printf("polling for new messages: %v", s.queue) + newMsgs, err := s.poller.PollForNewMessages(ctx, s.queue) + if err != nil { + return errors.Wrap(err, "unable to poll for messages") + } + + for _, msg := range newMsgs { + if err := s.store.Save(ctx, msg); err != nil { + log.Println("warn: unable to save new message %v", err) + continue + } + } + + s.bus.Fire("new-messages", newMsgs) + } + return nil +} diff --git a/internal/sqs-browse/ui/events.go b/internal/sqs-browse/ui/events.go new file mode 100644 index 0000000..b7844fb --- /dev/null +++ b/internal/sqs-browse/ui/events.go @@ -0,0 +1,5 @@ +package ui + +import "github.com/lmika/awstools/internal/sqs-browse/models" + +type NewMessagesEvent []*models.Message \ No newline at end of file diff --git a/internal/sqs-browse/ui/model.go b/internal/sqs-browse/ui/model.go new file mode 100644 index 0000000..637e053 --- /dev/null +++ b/internal/sqs-browse/ui/model.go @@ -0,0 +1,80 @@ +package ui + +import ( + table "github.com/calyptia/go-bubble-table" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type uiModel struct { + table table.Model + viewport viewport.Model + + tableRows []table.Row +} + +func NewModel() tea.Model { + tbl := table.New([]string{"seq", "message"}, 100, 20) + rows := make([]table.Row, 0) + tbl.SetRows(rows) + + vprt := viewport.New(100, 15) + + model := uiModel{ + table: tbl, + viewport: vprt, + tableRows: rows, + } + + return model +} + +func (m uiModel) Init() tea.Cmd { + return nil +} + +func (m *uiModel) updateViewportToSelectedMessage() { + if message, ok := m.table.SelectedRow().(messageTableRow); ok { + m.viewport.SetContent(message.Data) + } else { + m.viewport.SetContent("(no message selected)") + } +} + +func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case NewMessagesEvent: + for _, newMsg := range msg { + m.tableRows = append(m.tableRows, messageTableRow(*newMsg)) + } + m.table.SetRows(m.tableRows) + m.updateViewportToSelectedMessage() + + case tea.KeyMsg: + + switch msg.String() { + + case "ctrl+c", "q": + return m, tea.Quit + case "up", "i": + m.table.GoUp() + m.updateViewportToSelectedMessage() + case "down", "k": + m.table.GoDown() + m.updateViewportToSelectedMessage() + } + } + + updatedTable, tableMsgs := m.table.Update(nil) + updatedViewport, viewportMsgs := m.viewport.Update(msg) + + m.table = updatedTable + m.viewport = updatedViewport + + return m, tea.Batch(tableMsgs, viewportMsgs) +} + +func (m uiModel) View() string { + return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.viewport.View()) +} diff --git a/internal/sqs-browse/ui/tblmodel.go b/internal/sqs-browse/ui/tblmodel.go new file mode 100644 index 0000000..1dc2e1a --- /dev/null +++ b/internal/sqs-browse/ui/tblmodel.go @@ -0,0 +1,26 @@ +package ui + +import ( + "fmt" + "github.com/lmika/awstools/internal/sqs-browse/models" + table "github.com/calyptia/go-bubble-table" + "io" + "strings" +) + +type messageTableRow models.Message + +func (mtr messageTableRow) Render(w io.Writer, model table.Model, index int) { + firstLine := strings.SplitN(string(mtr.Data), "\n", 2)[0] + + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("%d", mtr.ID)) + sb.WriteString("\t") + sb.WriteString(firstLine) + + if index == model.Cursor() { + fmt.Fprintln(w, model.Styles.SelectedRow.Render(sb.String())) + } else { + fmt.Fprintln(w, sb.String()) + } +}