diff --git a/.gitignore b/.gitignore index 72b27cf..732e2b1 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ localstack /spare /cfn .envrc +/t-rec* diff --git a/go.mod b/go.mod index 2e6aec7..22b0fdf 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 github.com/charmbracelet/bubbles v0.17.1 github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.9.1 github.com/charmbracelet/log v0.3.1 github.com/fatih/color v1.16.0 github.com/google/go-cmp v0.6.0 @@ -48,7 +49,7 @@ require ( github.com/aws/smithy-go v1.19.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/caarlos0/env/v9 v9.0.0 // indirect - github.com/charmbracelet/lipgloss v0.9.1 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/google/subcommands v1.0.1 // indirect diff --git a/go.sum b/go.sum index 8a938e8..25be9ca 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRv github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw= diff --git a/ui/common.go b/ui/common.go index 6fde73e..4e87e37 100644 --- a/ui/common.go +++ b/ui/common.go @@ -39,6 +39,20 @@ func Checkbox(label string, checked bool) string { return fmt.Sprintf("[ ] %s", label) } +// ToggleWidget represents a toggle. +func ToggleWidget(label string, now, enabled bool) string { + if now { + if enabled { + return ColorFg("▶ [x] "+label, "212") + } + return ColorFg("▶ [ ] "+label, "212") + } + if enabled { + return ColorFg(" [x] "+label, "212") + } + return fmt.Sprintf(" [ ] %s", label) +} + // Split splits a string into multiple lines. // Each line has a maximum length of 80 characters. func Split(s string) []string { @@ -103,3 +117,32 @@ func (c *Choice) Decrement() { c.Choice = c.Max } } + +// Toggle represents a toggle. +type Toggle struct { + Enabled bool +} + +// NewToggle returns a new toggle. +func NewToggle() *Toggle { + return &Toggle{ + Enabled: false, + } +} + +// Toggle toggles the toggle. +func (t *Toggle) Toggle() { + t.Enabled = !t.Enabled +} + +// ToggleSets represents a set of toggles. +type ToggleSets []*Toggle + +// NewToggleSets returns a new toggle sets. +func NewToggleSets(n int) ToggleSets { + ts := make([]*Toggle, 0, n) + for i := 0; i < n; i++ { + ts = append(ts, NewToggle()) + } + return ts +} diff --git a/ui/s3hub/command.go b/ui/s3hub/command.go new file mode 100644 index 0000000..9248cab --- /dev/null +++ b/ui/s3hub/command.go @@ -0,0 +1,113 @@ +package s3hub + +import ( + "context" + "crypto/rand" + "fmt" + "math/big" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/nao1215/rainbow/app/di" + "github.com/nao1215/rainbow/app/domain/model" + "github.com/nao1215/rainbow/app/usecase" + "github.com/nao1215/rainbow/ui" + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" +) + +// fetchS3BucketMsg is the message that is sent when the user wants to fetch the list of the S3 buckets. +type fetchS3BucketMsg struct { + buckets model.BucketSets +} + +// fetchS3BucketListCmd fetches the list of the S3 buckets. +func fetchS3BucketListCmd(ctx context.Context, app *di.S3App) tea.Cmd { + return tea.Cmd(func() tea.Msg { + output, err := app.S3BucketLister.ListS3Buckets(ctx, &usecase.S3BucketListerInput{}) + if err != nil { + return ui.ErrMsg(err) + } + return fetchS3BucketMsg{ + buckets: output.Buckets, + } + }) +} + +type deleteS3BucketMsg struct { + deletedBucket model.Bucket +} + +// deleteS3BucketCmd deletes the S3 bucket. +// TODO: refactor +func deleteS3BucketCmd(ctx context.Context, app *di.S3App, bucket model.Bucket) tea.Cmd { + d, err := rand.Int(rand.Reader, big.NewInt(500)) + if err != nil { + return func() tea.Msg { + return ui.ErrMsg(fmt.Errorf("failed to start deleting s3 bucket: %w", err)) + } + } + delay := time.Millisecond * time.Duration(d.Int64()) + + return tea.Tick(delay, func(t time.Time) tea.Msg { + output, err := app.S3ObjectsLister.ListS3Objects(ctx, &usecase.S3ObjectsListerInput{ + Bucket: bucket, + }) + if err != nil { + return err + } + + if len(output.Objects) != 0 { + eg, ctx := errgroup.WithContext(ctx) + sem := semaphore.NewWeighted(model.MaxS3DeleteObjectsParallelsCount) + chunks := divideIntoChunks(output.Objects, model.S3DeleteObjectChunksSize) + + for _, chunk := range chunks { + chunk := chunk // Create a new variable to avoid concurrency issues + // Acquire semaphore to control the number of concurrent goroutines + if err := sem.Acquire(ctx, 1); err != nil { + return err + } + + eg.Go(func() error { + defer sem.Release(1) + if _, err := app.S3ObjectsDeleter.DeleteS3Objects(ctx, &usecase.S3ObjectsDeleterInput{ + Bucket: bucket, + S3ObjectSets: chunk, + }); err != nil { + return err + } + return nil + }) + } + + if err := eg.Wait(); err != nil { + return err + } + } + + _, err = app.S3BucketDeleter.DeleteS3Bucket(ctx, &usecase.S3BucketDeleterInput{ + Bucket: bucket, + }) + if err != nil { + return ui.ErrMsg(err) + } + return deleteS3BucketMsg{ + deletedBucket: bucket, + } + }) +} + +// divideIntoChunks divides a slice into chunks of the specified size. +func divideIntoChunks(slice []model.S3ObjectIdentifier, chunkSize int) [][]model.S3ObjectIdentifier { + var chunks [][]model.S3ObjectIdentifier + + for i := 0; i < len(slice); i += chunkSize { + end := i + chunkSize + if end > len(slice) { + end = len(slice) + } + chunks = append(chunks, slice[i:end]) + } + return chunks +} diff --git a/ui/s3hub/create.go b/ui/s3hub/create.go index c90d8b2..55c41ca 100644 --- a/ui/s3hub/create.go +++ b/ui/s3hub/create.go @@ -135,7 +135,7 @@ func (m *s3hubCreateBucketModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - if m.choice == s3hubCreateBucketBucketNameChoice { + if m.state != s3hubCreateBucketStateCreated && m.choice == s3hubCreateBucketBucketNameChoice { var cmd tea.Cmd m.bucketNameInput, cmd = m.bucketNameInput.Update(msg) return m, cmd diff --git a/ui/s3hub/delete.go b/ui/s3hub/delete.go index ec8b676..1b6fa4e 100644 --- a/ui/s3hub/delete.go +++ b/ui/s3hub/delete.go @@ -1,15 +1,57 @@ package s3hub import ( + "context" "fmt" + "strings" + "github.com/charmbracelet/bubbles/progress" + "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/fatih/color" + "github.com/nao1215/rainbow/app/di" + "github.com/nao1215/rainbow/app/domain/model" "github.com/nao1215/rainbow/ui" ) type s3hubDeleteContentsModel struct { - // quitting is true when the user has quit the application. - quitting bool + // awsConfig is the AWS configuration. + awsConfig *model.AWSConfig + // awsProfile is the AWS profile. + awsProfile model.AWSProfile + // region is the AWS region that the user wants to create the S3 bucket. + region model.Region + // choice is the currently selected menu item. + choice *ui.Choice + // app is the S3 application service. + app *di.S3App + // ctx is the context. + ctx context.Context +} + +// s3hubDeleteContentsStatus is the status of the delete contents operation. +func newS3hubDeleteContentsModel() (*s3hubDeleteContentsModel, error) { + ctx := context.Background() + profile := model.NewAWSProfile("") + cfg, err := model.NewAWSConfig(ctx, profile, "") + if err != nil { + return nil, err + } + region := cfg.Region() + + app, err := di.NewS3App(ctx, profile, region) + if err != nil { + return nil, err + } + + return &s3hubDeleteContentsModel{ + awsConfig: cfg, + awsProfile: profile, + region: region, + app: app, + ctx: ctx, + }, nil } func (m *s3hubDeleteContentsModel) Init() tea.Cmd { @@ -17,13 +59,6 @@ func (m *s3hubDeleteContentsModel) Init() tea.Cmd { } func (m *s3hubDeleteContentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - if msg, ok := msg.(tea.KeyMsg); ok { - k := msg.String() - if k == "q" || k == "esc" || k == "ctrl+c" { - m.quitting = true - return m, tea.Quit - } - } return m, nil } @@ -34,29 +69,274 @@ func (m *s3hubDeleteContentsModel) View() string { ui.Subtle("j/k, up/down: select")+" | "+ui.Subtle("enter: choose")+" | "+ui.Subtle("q, esc: quit")) } +// s3hubDeleteBucketStatus is the status of the delete bucket operation. +type s3hubDeleteBucketStatus int + +const ( + // s3hubDeleteBucketStatusNone is the status when the delete bucket operation is not executed. + s3hubDeleteBucketStatusNone s3hubDeleteBucketStatus = iota + // s3hubDeleteBucketStatusBucketDeleting is the status when the delete bucket operation is executed and the bucket is being deleted. + s3hubDeleteBucketStatusBucketDeleting + // s3hubDeleteBucketStatusBucketDeleted is the status when the delete bucket operation is executed and the bucket is deleted. + s3hubDeleteBucketStatusBucketDeleted +) + +var ( + currentBucketNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) + doneStyle = lipgloss.NewStyle().Margin(2, 1, 1) + checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") +) + type s3hubDeleteBucketModel struct { - // quitting is true when the user has quit the application. - quitting bool + // awsConfig is the AWS configuration. + awsConfig *model.AWSConfig + // awsProfile is the AWS profile. + awsProfile model.AWSProfile + // region is the AWS region that the user wants to create the S3 bucket. + region model.Region + // choice is the currently selected menu item. + choice *ui.Choice + // toggle is the currently selected menu item. + toggles ui.ToggleSets + // app is the S3 application service. + app *di.S3App + // bucketSets is the list of the S3 buckets. + bucketSets model.BucketSets + // targetBuckets is the list of the S3 buckets that the user wants to delete. + targetBuckets []model.Bucket + // s3bucketListStatus is the status of the list bucket operation. + s3bucketListStatus s3hubListBucketStatus + // s3bucketDeleteStatus is the status of the delete bucket operation. + s3bucketDeleteStatus s3hubDeleteBucketStatus + // ctx is the context. + ctx context.Context + // err is the error that occurred during the operation. + err error + + // TODO: refactor + index int + sum int + width int + height int + spinner spinner.Model + progress progress.Model +} + +// s3hubDeleteBucketStatus is the status of the delete bucket operation. +func newS3hubDeleteBucketModel() (*s3hubDeleteBucketModel, error) { + ctx := context.Background() + profile := model.NewAWSProfile("") + cfg, err := model.NewAWSConfig(ctx, profile, "") + if err != nil { + return nil, err + } + region := cfg.Region() + + app, err := di.NewS3App(ctx, profile, region) + if err != nil { + return nil, err + } + + p := progress.New( + progress.WithDefaultGradient(), + progress.WithWidth(40), + progress.WithoutPercentage(), + ) + s := spinner.New() + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) + + return &s3hubDeleteBucketModel{ + awsConfig: cfg, + awsProfile: profile, + region: region, + toggles: ui.NewToggleSets(0), + app: app, + ctx: ctx, + s3bucketListStatus: s3hubListBucketStatusNone, + spinner: s, + progress: p, + index: 1, + }, nil } func (m *s3hubDeleteBucketModel) Init() tea.Cmd { - return nil + return nil // Not called this method } func (m *s3hubDeleteBucketModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - if msg, ok := msg.(tea.KeyMsg); ok { - k := msg.String() - if k == "q" || k == "esc" || k == "ctrl+c" { - m.quitting = true + if m.err != nil { + return m, tea.Quit + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "j", "down": + m.choice.Increment() + case "k", "up": + m.choice.Decrement() + case "ctrl+c": + m.s3bucketListStatus = s3hubListBucketStatusQuit return m, tea.Quit + case "q", "esc": + m.s3bucketListStatus = s3hubListBucketStatusReturnToTop + return newRootModel(), nil + case "enter": + if m.s3bucketListStatus == s3hubListBucketStatusReturnToTop || m.s3bucketDeleteStatus == s3hubDeleteBucketStatusBucketDeleted { + return newRootModel(), nil + } + + if m.s3bucketListStatus == s3hubListBucketStatusBucketListed && m.s3bucketDeleteStatus == s3hubDeleteBucketStatusNone { + m.targetBuckets = make([]model.Bucket, 0, len(m.toggles)) + for i, t := range m.toggles { + if t.Enabled { + m.targetBuckets = append(m.targetBuckets, m.bucketSets[i].Bucket) + } + } + if len(m.targetBuckets) == 0 { + return m, nil + } + m.sum = len(m.targetBuckets) + 1 + m.s3bucketDeleteStatus = s3hubDeleteBucketStatusBucketDeleting + return m, tea.Batch(m.spinner.Tick, deleteS3BucketCmd(m.ctx, m.app, m.targetBuckets[0])) + } + case " ": + if m.s3bucketListStatus == s3hubListBucketStatusBucketListed && m.s3bucketDeleteStatus == s3hubDeleteBucketStatusNone { + m.toggles[m.choice.Choice].Toggle() + } } + case tea.WindowSizeMsg: + m.width, m.height = msg.Width, msg.Height + case fetchS3BucketMsg: + m.s3bucketListStatus = s3hubListBucketStatusBucketCreated + m.bucketSets = msg.buckets + m.choice = ui.NewChoice(0, m.bucketSets.Len()-1) + m.toggles = ui.NewToggleSets(m.bucketSets.Len()) + return m, nil + case deleteS3BucketMsg: + m.targetBuckets = m.targetBuckets[1:] + if len(m.targetBuckets) == 0 { + m.s3bucketDeleteStatus = s3hubDeleteBucketStatusBucketDeleted + return m, nil + } + progressCmd := m.progress.SetPercent(float64(m.index) / float64(m.sum-1)) + m.index++ + return m, tea.Batch( + progressCmd, + tea.Printf("%s %s", checkMark, m.targetBuckets[0]), + deleteS3BucketCmd(m.ctx, m.app, m.targetBuckets[0])) + case spinner.TickMsg: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + case progress.FrameMsg: + newModel, cmd := m.progress.Update(msg) + if newModel, ok := newModel.(progress.Model); ok { + m.progress = newModel + } + return m, cmd + case ui.ErrMsg: + m.err = msg + m.s3bucketListStatus = s3hubListBucketStatusQuit + return m, tea.Quit + default: + return m, nil } return m, nil } func (m *s3hubDeleteBucketModel) View() string { - return fmt.Sprintf( - "%s\n%s", - "s3hubDeleteBucketModel", - ui.Subtle("j/k, up/down: select")+" | "+ui.Subtle("enter: choose")+" | "+ui.Subtle("q, esc: quit")) + if m.err != nil { + m.s3bucketListStatus = s3hubListBucketStatusQuit + return ui.ErrorMessage(m.err) + } + + if m.s3bucketListStatus == s3hubListBucketStatusQuit { + return ui.GoodByeMessage() + } + + if m.s3bucketDeleteStatus == s3hubDeleteBucketStatusBucketDeleted { + return doneStyle.Render("All S3 buckets deleted. Press to return to the top.\n") + } + + if m.s3bucketDeleteStatus == s3hubDeleteBucketStatusBucketDeleting { + w := lipgloss.Width(fmt.Sprintf("%d", m.sum)) + bucketCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, m.sum-1) + + spin := m.spinner.View() + " " + prog := m.progress.View() + cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+bucketCount)) + + bucketName := currentBucketNameStyle.Render(m.targetBuckets[0].String()) + info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Deleting " + bucketName) + cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+bucketCount)) + gap := strings.Repeat(" ", cellsRemaining) + return spin + info + gap + prog + bucketCount + } + + if m.s3bucketListStatus == s3hubListBucketStatusNone || m.s3bucketListStatus == s3hubListBucketStatusBucketCreating { + return fmt.Sprintf( + "fetching the list of the S3 buckets (profile=%s)\n", + m.awsProfile.String()) + } + + if m.s3bucketListStatus == s3hubListBucketStatusBucketCreated { + return m.bucketListString() + } + return m.bucketListString() // TODO: implement +} + +// bucketListString returns the string representation of the bucket list. +func (m *s3hubDeleteBucketModel) bucketListString() string { + switch len(m.bucketSets) { + case 0: + return m.emptyBucketListString() + default: + return m.bucketListStrWithCheckbox() + } +} + +// bucketListStrWithCheckbox generates the string representation of the bucket list. +func (m *s3hubDeleteBucketModel) bucketListStrWithCheckbox() string { + startIndex := 0 + endIndex := len(m.bucketSets) + + if m.choice.Choice >= windowHeight { + startIndex = m.choice.Choice - windowHeight + 1 + endIndex = startIndex + windowHeight + if endIndex > len(m.bucketSets) { + startIndex = len(m.bucketSets) - windowHeight + endIndex = len(m.bucketSets) + } + } else { + if len(m.bucketSets) > windowHeight { + endIndex = windowHeight + } + } + + m.s3bucketListStatus = s3hubListBucketStatusBucketListed + s := fmt.Sprintf("Select the S3 bucket(s) you want to delete %d/%d (profile=%s)\n\n", + m.choice.Choice+1, m.bucketSets.Len(), m.awsProfile.String()) + for i := startIndex; i < endIndex; i++ { + b := m.bucketSets[i] + s += fmt.Sprintf("%s\n", + ui.ToggleWidget( + fmt.Sprintf( + "%s (region=%s, updated_at=%s)", + color.GreenString("%s", b.Bucket), + color.YellowString("%s", b.Region), + b.CreationDate.Format("2006-01-02 15:04:05 MST")), + m.choice.Choice == i, m.toggles[i].Enabled)) + } + s += ui.Subtle("\n: return to the top | : quit | up/down: select\n") + s += ui.Subtle(": choose bucket | delete choosed bucket\n\n") + return s +} + +// emptyBucketListString returns the string representation when there are no S3 buckets. +func (m *s3hubDeleteBucketModel) emptyBucketListString() string { + m.s3bucketListStatus = s3hubListBucketStatusReturnToTop + return fmt.Sprintf("No S3 buckets (profile=%s)\n\n%s\n", + m.awsProfile.String(), + ui.Subtle(": return to the top")) } diff --git a/ui/s3hub/list.go b/ui/s3hub/list.go index e6f6f3d..e93780d 100644 --- a/ui/s3hub/list.go +++ b/ui/s3hub/list.go @@ -8,7 +8,6 @@ import ( "github.com/fatih/color" "github.com/nao1215/rainbow/app/di" "github.com/nao1215/rainbow/app/domain/model" - "github.com/nao1215/rainbow/app/usecase" "github.com/nao1215/rainbow/ui" ) @@ -36,9 +35,6 @@ type s3hubListBucketModel struct { // s3hubListBucketStatus is the status of the list bucket operation. type s3hubListBucketStatus int -// fetchMsg is the message that is sent when the user wants to fetch the list of the S3 buckets. -type fetchMsg struct{} - const ( // s3hubListBucketStatusNone is the status when the list bucket operation is not executed. s3hubListBucketStatusNone s3hubListBucketStatus = iota @@ -112,11 +108,17 @@ func (m *s3hubListBucketModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.status == s3hubListBucketStatusReturnToTop { return newRootModel(), nil } + case "space": + // TODO: implement } - case fetchMsg: + case fetchS3BucketMsg: + m.status = s3hubListBucketStatusBucketCreated + m.bucketSets = msg.buckets + m.choice = ui.NewChoice(0, m.bucketSets.Len()-1) return m, nil case ui.ErrMsg: m.err = msg + m.status = s3hubListBucketStatusQuit return m, tea.Quit default: return m, nil @@ -175,7 +177,7 @@ func (m *s3hubListBucketModel) bucketListStrWithCheckbox() string { } m.status = s3hubListBucketStatusBucketListed - s := fmt.Sprintf("S3 buckets %d/%d (profile=%s)\n", m.choice.Choice+1, m.bucketSets.Len(), m.awsProfile.String()) + s := fmt.Sprintf("S3 buckets %d/%d (profile=%s)\n\n", m.choice.Choice+1, m.bucketSets.Len(), m.awsProfile.String()) for i := startIndex; i < endIndex; i++ { b := m.bucketSets[i] s += fmt.Sprintf("%s\n", @@ -188,7 +190,7 @@ func (m *s3hubListBucketModel) bucketListStrWithCheckbox() string { m.choice.Choice == i)) } s += ui.Subtle("\n: return to the top | : quit | up/down: select\n") - s += ui.Subtle(": choose bucket\n\n") + s += ui.Subtle(", : choose bucket\n\n") return s } @@ -199,21 +201,3 @@ func (m *s3hubListBucketModel) emptyBucketListString() string { m.awsProfile.String(), ui.Subtle(": return to the top")) } - -// fetchS3BucketListCmd fetches the list of the S3 buckets. -func (m *s3hubListBucketModel) fetchS3BucketListCmd() tea.Cmd { - return tea.Cmd(func() tea.Msg { - m.status = s3hubListBucketStatusBucketCreating - - output, err := m.app.S3BucketLister.ListS3Buckets(m.ctx, &usecase.S3BucketListerInput{}) - if err != nil { - m.status = s3hubListBucketStatusQuit - return ui.ErrMsg(err) - } - m.bucketSets = output.Buckets - m.status = s3hubListBucketStatusBucketCreated - m.choice = ui.NewChoice(0, len(m.bucketSets)-1) - - return fetchMsg{} - }) -} diff --git a/ui/s3hub/root.go b/ui/s3hub/root.go index cb563e3..53ba11c 100644 --- a/ui/s3hub/root.go +++ b/ui/s3hub/root.go @@ -110,13 +110,20 @@ func (m *s3hubRootModel) updateChoices(msg tea.Msg) (tea.Model, tea.Cmd) { m.err = err return m, tea.Quit } - return model, model.fetchS3BucketListCmd() + model.status = s3hubListBucketStatusBucketCreating + return model, fetchS3BucketListCmd(model.ctx, model.app) case s3hubTopCopyChoice: return &s3hubCopyModel{}, nil case s3hubTopDeleteContentsChoice: return &s3hubDeleteContentsModel{}, nil case s3hubTopDeleteBucketChoice: - return &s3hubDeleteBucketModel{}, nil + model, err := newS3hubDeleteBucketModel() + if err != nil { + m.err = err + return m, tea.Quit + } + model.s3bucketListStatus = s3hubListBucketStatusBucketCreating + return model, fetchS3BucketListCmd(model.ctx, model.app) } } } @@ -126,7 +133,7 @@ func (m *s3hubRootModel) updateChoices(msg tea.Msg) (tea.Model, tea.Cmd) { // choicesView returns a string containing the choices menu. func (m *s3hubRootModel) choicesView() string { c := m.choice.Choice - template := "%s\n\n" + template := "%s\n" template += ui.Subtle("j/k, up/down: select | enter: choose | q, : quit") choices := fmt.Sprintf(