diff --git a/cmd/tea.go b/cmd/tea.go index e4049c04..5a68dc66 100644 --- a/cmd/tea.go +++ b/cmd/tea.go @@ -50,7 +50,7 @@ type FinalReportingModel interface { FinalReport() string } -func CmdWrapper(action string, requiredScopes []string, commandModel func([]string) tea.Model) func(cmd *cobra.Command, args []string) { +func CmdWrapper(action string, requiredScopes []string, commandModel func(args []string, execCommandFunc ExecCommandFunc) tea.Model) func(cmd *cobra.Command, args []string) { return func(cmd *cobra.Command, args []string) { // set up a context for the command ctx, cancel := context.WithCancel(context.Background()) @@ -95,7 +95,7 @@ func CmdWrapper(action string, requiredScopes []string, commandModel func([]stri return err } - p := tea.NewProgram(cmdModel{ + m := cmdModel{ action: action, ctx: ctx, cancel: cancel, @@ -104,14 +104,15 @@ func CmdWrapper(action string, requiredScopes []string, commandModel func([]stri requiredScopes: requiredScopes, apiKey: viper.GetString("api-key"), tasks: map[string]tea.Model{}, - cmd: commandModel(args), - }) + } + m.cmd = commandModel(args, m.NewExecCommand) + p := tea.NewProgram(&m) result, err := p.Run() if err != nil { return fmt.Errorf("could not start program: %w", err) } - cmd, ok := result.(cmdModel) + cmd, ok := result.(*cmdModel) if ok { frm, ok := cmd.cmd.(FinalReportingModel) if ok { diff --git a/cmd/tea_ensuretoken.go b/cmd/tea_ensuretoken.go index 31d736c2..43cd4da9 100644 --- a/cmd/tea_ensuretoken.go +++ b/cmd/tea_ensuretoken.go @@ -96,7 +96,7 @@ func (m ensureTokenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case displayAuthorizationInstructionsMsg: m.config = msg.config m.deviceCode = msg.deviceCode - + m.status = taskStatusDone // avoid console flickering to allow click to be registered m.title = "Manual device authorization." beginAuthMessage := `# Authenticate with a browser @@ -133,26 +133,23 @@ Then enter the code: case tokenLoadedMsg: m.status = taskStatusDone m.title = "Using stored token" + m.deviceMessage = "" return m, m.tokenAvailable(msg.token) case tokenReceivedMsg: m.status = taskStatusDone m.title = "Authentication successful, using API key" + m.deviceMessage = "" return m, m.tokenAvailable(msg.token) case tokenStoredMsg: m.status = taskStatusDone m.title = fmt.Sprintf("Authentication successful, token stored locally (%v)", msg.file) + m.deviceMessage = "" return m, m.tokenAvailable(msg.token) case otherError: if msg.id == m.spinner.ID() { m.errors = append(m.errors, fmt.Sprintf("Note: %v", msg.err)) } return m, nil - case fatalError: - if msg.id == m.spinner.ID() { - m.status = taskStatusError - m.title = markdownToString(fmt.Sprintf("Ensuring Token Error: %v", msg.err)) - } - return m, nil default: var taskCmd tea.Cmd m.taskModel, taskCmd = m.taskModel.Update(msg) @@ -165,7 +162,7 @@ func (m ensureTokenModel) View() string { if len(m.errors) > 0 { view += fmt.Sprintf("\n%v\n", strings.Join(m.errors, "\n")) } - if m.deviceMessage != "" && !(m.status == taskStatusDone || m.status == taskStatusError) { + if m.deviceMessage != "" { view += fmt.Sprintf("\n%v\n", m.deviceMessage) } return view @@ -272,32 +269,41 @@ func (m ensureTokenModel) awaitTokenCmd() tea.Msg { defer cancel() } - // while the RFC requires the oauth2 library to use 5 as the default, - // Auth0 should be able to handle more. Hence we re-implement the + // while the RFC requires the oauth2 library to use 5 as the default, Auth0 + // should be able to handle more. Hence we re-implement the m.deviceCode.Interval = 1 var token *oauth2.Token var err error for { + log.Trace("attempting to get token from auth0") // reset the deviceCode's expiry to at most 1.5 seconds m.deviceCode.Expiry = time.Now().Add(1500 * time.Millisecond) token, err = m.config.DeviceAccessToken(ctx, m.deviceCode) if err == nil { // we got a token, continue below. kthxbye + log.Trace("we got a token from auth0") break } - if errors.Is(err, context.DeadlineExceeded) { + // See https://github.com/golang/oauth2/issues/635, + // https://github.com/golang/oauth2/pull/636, + // https://go-review.googlesource.com/c/oauth2/+/476316 + if errors.Is(err, context.DeadlineExceeded) || strings.HasSuffix(err.Error(), "context deadline exceeded") { // the context has expired, we need to retry + log.WithError(err).Trace("context.DeadlineExceeded - waiting for a second") + time.Sleep(time.Second) continue } // re-implement DeviceAccessToken's logic, but faster - e, ok := err.(*oauth2.RetrieveError) // nolint:errorlint // we depend on DeviceAccessToken() returning an non-wrapped error - if !ok { + e, isRetrieveError := err.(*oauth2.RetrieveError) // nolint:errorlint // we depend on DeviceAccessToken() returning an non-wrapped error + if !isRetrieveError { + log.WithError(err).Trace("error authorizing token") return fatalError{id: m.spinner.ID(), err: fmt.Errorf("error authorizing token: %w", err)} } + switch e.ErrorCode { case "slow_down": // // https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 @@ -310,7 +316,6 @@ func (m ensureTokenModel) awaitTokenCmd() tea.Msg { default: return fatalError{id: m.spinner.ID(), err: fmt.Errorf("error authorizing token (%v): %w", e.ErrorCode, err)} } - } span := trace.SpanFromContext(m.ctx) diff --git a/cmd/tea_execcommand.go b/cmd/tea_execcommand.go new file mode 100644 index 00000000..e502e671 --- /dev/null +++ b/cmd/tea_execcommand.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "fmt" + "io" + "os/exec" + + tea "github.com/charmbracelet/bubbletea" +) + +type ExecCommandFunc func(cmd *exec.Cmd) tea.ExecCommand + +// NewExecCommand returns a new ExecCommand that will print the last view from +// the parent cmdModel after bubbletea has released the terminal, but before the +// command is run. +func (m *cmdModel) NewExecCommand(c *exec.Cmd) tea.ExecCommand { + return NewExecCommand(m, c) +} + +func NewExecCommand(parent *cmdModel, c *exec.Cmd) *cliExecCommandModel { + return &cliExecCommandModel{ + parent: parent, + Cmd: c, + } +} + +// osExecCommand is a layer over an exec.Cmd that satisfies the ExecCommand +// interface. It prints the last view from +// the parent cmdModel after bubbletea has released the terminal, but before the +// command is run. +type cliExecCommandModel struct { + parent *cmdModel + *exec.Cmd +} + +func (c cliExecCommandModel) Run() error { + _, err := c.Stdout.Write([]byte(c.parent.frozenView)) + if err != nil { + return fmt.Errorf("failed to write view to stdout: %w", err) + } + return c.Cmd.Run() +} + +// SetStdin sets stdin on underlying exec.Cmd to the given io.Reader. +func (c *cliExecCommandModel) SetStdin(r io.Reader) { + // If unset, have the command use the same input as the terminal. + if c.Stdin == nil { + c.Stdin = r + } +} + +// SetStdout sets stdout on underlying exec.Cmd to the given io.Writer. +func (c *cliExecCommandModel) SetStdout(w io.Writer) { + // If unset, have the command use the same output as the terminal. + if c.Stdout == nil { + c.Stdout = w + } +} + +// SetStderr sets stderr on the underlying exec.Cmd to the given io.Writer. +func (c *cliExecCommandModel) SetStderr(w io.Writer) { + // If unset, use stderr for the command's stderr + if c.Stderr == nil { + c.Stderr = w + } +} diff --git a/cmd/tea_initialisesources.go b/cmd/tea_initialisesources.go index f097e79b..3c8490e4 100644 --- a/cmd/tea_initialisesources.go +++ b/cmd/tea_initialisesources.go @@ -156,17 +156,12 @@ func (m initialiseSourcesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.id == m.spinner.ID() { m.errors = append(m.errors, fmt.Sprintf("Note: %v", msg.err)) } - case fatalError: - if msg.id == m.spinner.ID() { - m.status = taskStatusError - m.title = markdownToString(fmt.Sprintf("> error while configuring AWS access: %v", msg.err)) - } - default: - var taskCmd tea.Cmd - m.taskModel, taskCmd = m.taskModel.Update(msg) - cmds = append(cmds, taskCmd) } + var taskCmd tea.Cmd + m.taskModel, taskCmd = m.taskModel.Update(msg) + cmds = append(cmds, taskCmd) + // process the form if it is not yet done if m.awsConfigForm != nil && !m.awsConfigFormDone { switch m.awsConfigForm.State { diff --git a/cmd/tea_plan.go b/cmd/tea_plan.go index 3279ab11..7c2df4ae 100644 --- a/cmd/tea_plan.go +++ b/cmd/tea_plan.go @@ -22,19 +22,21 @@ type runPlanModel struct { args []string planFile string - revlinkTask revlinkWarmupModel + execCommandFunc ExecCommandFunc + revlinkTask revlinkWarmupModel taskModel } type runPlanNowMsg struct{} type runPlanFinishedMsg struct{} -func NewRunPlanModel(args []string, planFile string) runPlanModel { +func NewRunPlanModel(args []string, planFile string, execCommandFunc ExecCommandFunc) runPlanModel { return runPlanModel{ args: args, planFile: planFile, - revlinkTask: NewRevlinkWarmupModel(), - taskModel: NewTaskModel("Planning Changes"), + revlinkTask: NewRevlinkWarmupModel(), + execCommandFunc: execCommandFunc, + taskModel: NewTaskModel("Planning Changes"), } } @@ -77,22 +79,26 @@ func (m runPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { c = exec.CommandContext(m.ctx, "bash", "-c", "for i in $(seq 100); do echo fake terraform plan progress line $i of 100; done; sleep 1") } - _, span := tracing.Tracer().Start(m.ctx, "terraform plan", trace.WithAttributes( + _, span := tracing.Tracer().Start(m.ctx, "terraform plan", trace.WithAttributes( // nolint:spancheck // will be ended in the tea.Exec cleanup func attribute.String("command", strings.Join(m.args, " ")), )) - cmds = append(cmds, tea.ExecProcess( - c, - func(err error) tea.Msg { - defer span.End() - - if err != nil { - return fatalError{err: fmt.Errorf("failed to run terraform plan: %w", err)} - } - return runPlanFinishedMsg{} - })) + cmds = append(cmds, + tea.Sequence( + func() tea.Msg { return freezeViewMsg{} }, + tea.Exec( // nolint:spancheck // will be ended in the tea.Exec cleanup func + m.execCommandFunc(c), + func(err error) tea.Msg { + defer span.End() + + if err != nil { + return fatalError{err: fmt.Errorf("failed to run terraform plan: %w", err)} + } + return runPlanFinishedMsg{} + }))) case runPlanFinishedMsg: m.taskModel.status = taskStatusDone + cmds = append(cmds, func() tea.Msg { return unfreezeViewMsg{} }) default: // var cmd tea.Cmd diff --git a/cmd/tea_revlink.go b/cmd/tea_revlink.go index db1ac30b..67f538d5 100644 --- a/cmd/tea_revlink.go +++ b/cmd/tea_revlink.go @@ -66,10 +66,6 @@ func (m revlinkWarmupModel) Update(msg tea.Msg) (revlinkWarmupModel, tea.Cmd) { cmds = append(cmds, m.waitForStatusActivity) case revlinkWarmupFinishedMsg: m.taskModel.status = taskStatusDone - case fatalError: - if msg.id == m.spinner.ID() { - m.taskModel.status = taskStatusError - } default: var taskCmd tea.Cmd m.taskModel, taskCmd = m.taskModel.Update(msg) diff --git a/cmd/tea_terraform.go b/cmd/tea_terraform.go index 4c302ba6..bd937e38 100644 --- a/cmd/tea_terraform.go +++ b/cmd/tea_terraform.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "slices" "sort" "strings" "time" @@ -31,15 +32,23 @@ type cmdModel struct { requiredScopes []string // UI state - tasks map[string]tea.Model - terraformHasStarted bool // remember whether terraform already has started. this is important to do the correct workarounds on errors. See also `skipView()` - fatalErrorSeen bool // remember whether a fatalError has been seen to avoid showing pending tasks - fatalError string // this will get set if there's a fatalError coming through that doesn't have a task ID set + tasks map[string]tea.Model + fatalError string // this will get set if there's a fatalError coming through that doesn't have a task ID set + + frozen bool + frozenView string // this gets set if the view is frozen, and will be used to render the last view using the cliExecCommand + + hideStartupStatus bool // business logic. This model will implement the actual CLI functionality requested. cmd tea.Model } +type freezeViewMsg struct{} +type unfreezeViewMsg struct{} + +type hideStartupStatusMsg struct{} + type delayQuitMsg struct{} // fatalError is a wrapper for errors that should abort the running tea.Program. @@ -54,7 +63,7 @@ type otherError struct { err error } -func (m cmdModel) Init() tea.Cmd { +func (m *cmdModel) Init() tea.Cmd { // use the main cli context to not take this time from the main timeout m.tasks["00_oi"] = NewInstanceLoaderModel(m.ctx, m.app) m.tasks["01_token"] = NewEnsureTokenModel(m.ctx, m.app, m.apiKey, m.requiredScopes) @@ -87,10 +96,10 @@ func (m cmdModel) Init() tea.Cmd { ) } -func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { log.Debugf("cmdModel: Update %T received %#v", msg, msg) - batch := []tea.Cmd{} + cmds := []tea.Cmd{} // special case the messages that need to be handled at this level switch msg := msg.(type) { @@ -98,28 +107,22 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "ctrl+c" { return m, tea.Quit } + case freezeViewMsg: + m.frozenView = m.View() + m.frozen = true + case unfreezeViewMsg: + m.frozen = false + m.frozenView = "" + case hideStartupStatusMsg: + m.hideStartupStatus = true case fatalError: log.WithError(msg.err).WithField("msg.id", msg.id).Debug("cmdModel: fatalError received") - // skipView based on the previous view. While this is not perfect, it's - // the best we can currently do without taking complete control of - // terraform command i/o - if m.terraformHasStarted { - skipView(m.View()) - } - - m.fatalErrorSeen = true + // record the fatal error here, to repeat it at the end of the process + m.fatalError = msg.err.Error() - // record the fatal error here if it was not from a specific taskModel - if msg.id == 0 { - m.fatalError = msg.err.Error() - } - - return m, tea.Sequence( - tea.Batch(batch...), - tea.Quit, - ) + cmds = append(cmds, func() tea.Msg { return delayQuitMsg{} }) case instanceLoadedMsg: m.oi = msg.instance @@ -127,19 +130,12 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // delete(m.tasks, "00_oi") case tokenAvailableMsg: - tm, cmd := m.tokenChecks(msg.token) - batch = append(batch, cmd) - return tm, tea.Batch(batch...) - - case runPlanNowMsg, runTfApplyMsg: - m.terraformHasStarted = true - - case runPlanFinishedMsg, tfApplyFinishedMsg: - // bump screen after terraform ran - skipView(m.View()) + var cmd tea.Cmd + cmd = m.tokenChecks(msg.token) + cmds = append(cmds, cmd) case delayQuitMsg: - batch = append(batch, tea.Quit) + cmds = append(cmds, tea.Quit) } @@ -147,7 +143,7 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.cmd, cmd = m.cmd.Update(msg) if cmd != nil { - batch = append(batch, cmd) + cmds = append(cmds, cmd) } // pass all messages to all tasks @@ -155,35 +151,16 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { tm, cmd := t.Update(msg) m.tasks[k] = tm if cmd != nil { - batch = append(batch, cmd) + cmds = append(cmds, cmd) } } - return m, tea.Batch(batch...) -} - -// skipView scrolls the terminal contents up after ExecCommand() to avoid -// overwriting the output from terraform when rendering the next View(). this -// has to be used here in the cmdModel to catch the entire View() output. -// -// NOTE: this is quite brittle and _requires_ that the View() after terraform -// returned is at least many lines as the view before ExecCommand(), otherwise -// the difference will get eaten by bubbletea on re-rendering. -// -// TODO: make this hack less ugly -func skipView(view string) { - lines := strings.Split(view, "\n") - for range lines { - fmt.Println() - } - - // log.Debugf("printed %v lines:", len(lines)) - // log.Debug(lines) + return m, tea.Batch(cmds...) } -func (m cmdModel) tokenChecks(token *oauth2.Token) (cmdModel, tea.Cmd) { +func (m *cmdModel) tokenChecks(token *oauth2.Token) tea.Cmd { if viper.GetString("ovm-test-fake") != "" { - return m, func() tea.Msg { + return func() tea.Msg { return loadSourcesConfigMsg{ ctx: m.ctx, oi: m.oi, @@ -197,10 +174,10 @@ func (m cmdModel) tokenChecks(token *oauth2.Token) (cmdModel, tea.Cmd) { // permission auth0 will just not assign those scopes rather than fail ok, missing, err := HasScopesFlexible(token, m.requiredScopes) if err != nil { - return m, func() tea.Msg { return fatalError{err: fmt.Errorf("error checking token scopes: %w", err)} } + return func() tea.Msg { return fatalError{err: fmt.Errorf("error checking token scopes: %w", err)} } } if !ok { - return m, func() tea.Msg { + return func() tea.Msg { return fatalError{err: fmt.Errorf("authenticated successfully, but you don't have the required permission: '%v'", missing)} } } @@ -218,7 +195,7 @@ func (m cmdModel) tokenChecks(token *oauth2.Token) (cmdModel, tea.Cmd) { // for now, and we still need a good idea for a better way. Especially as // some of the models require access to viper (for GetConfig/SetConfig) or // contortions to store that data somewhere else. - return m, func() tea.Msg { + return func() tea.Msg { return loadSourcesConfigMsg{ ctx: m.ctx, oi: m.oi, @@ -229,35 +206,40 @@ func (m cmdModel) tokenChecks(token *oauth2.Token) (cmdModel, tea.Cmd) { } func (m cmdModel) View() string { - // show tasks in key order, skipping pending tasks to keep the ui uncluttered - allDone := true - tasks := make([]string, 0, len(m.tasks)) - keys := make([]string, 0, len(m.tasks)) - for k := range m.tasks { - keys = append(keys, k) + if m.frozen { + return "" } - sort.Strings(keys) - for _, k := range keys { - t, ok := m.tasks[k].(WithTaskModel) - if ok { - if t.TaskModel().status != taskStatusDone { - allDone = false - } - if t.TaskModel().status == taskStatusPending { - continue + bits := []string{} + + if !m.hideStartupStatus { + // show tasks in key order, skipping pending bits to keep the ui uncluttered + keys := make([]string, 0, len(m.tasks)) + for k := range m.tasks { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + t, ok := m.tasks[k].(WithTaskModel) + if ok { + if t.TaskModel().status == taskStatusPending { + continue + } } + bits = append(bits, m.tasks[k].View()) } - tasks = append(tasks, m.tasks[k].View()) } - if allDone { - // no need to show setup tasks after they're all done - tasks = []string{} - } - tasks = append(tasks, m.cmd.View()) + + bits = append(bits, m.cmd.View()) if m.fatalError != "" { - tasks = append(tasks, markdownToString(fmt.Sprintf("> Fatal Error: %v\n", m.fatalError))) + md := markdownToString(fmt.Sprintf("> Fatal Error: %v\n", m.fatalError)) + md, _ = strings.CutPrefix(md, "\n") + md, _ = strings.CutSuffix(md, "\n") + bits = append(bits, fmt.Sprintf("%v", md)) } - return strings.Join(tasks, "\n") + bits = slices.DeleteFunc(bits, func(s string) bool { + return s == "" || s == "\n" + }) + return strings.Join(bits, "\n") } var applyOnlyArgs = []string{ diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index 68222f71..6f219b35 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -61,7 +61,8 @@ type tfApplyModel struct { endingChangeSnapshot snapshotModel progress []string - width int + execCommandFunc ExecCommandFunc + width int } type startStartingSnapshotMsg struct{} @@ -73,7 +74,7 @@ type changeIdentifiedMsg struct { type runTfApplyMsg struct{} type tfApplyFinishedMsg struct{} -func NewTfApplyModel(args []string) tea.Model { +func NewTfApplyModel(args []string, execCommandFunc ExecCommandFunc) tea.Model { hasPlanSet := false autoapprove := false planFile := "overmind.plan" @@ -133,7 +134,7 @@ func NewTfApplyModel(args []string) tea.Model { planFile: planFile, needPlan: !hasPlanSet, - runPlanTask: NewRunPlanModel(planArgs, planFile), + runPlanTask: NewRunPlanModel(planArgs, planFile, execCommandFunc), runPlanFinished: hasPlanSet, submitPlanTask: NewSubmitPlanModel(planFile), @@ -145,6 +146,8 @@ func NewTfApplyModel(args []string) tea.Model { endingChange: make(chan tea.Msg, 10), // provide a small buffer for sending updates, so we don't block the processing endingChangeSnapshot: NewSnapShotModel("Ending Change", "indexing resources"), progress: []string{}, + + execCommandFunc: execCommandFunc, } } @@ -195,6 +198,8 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } }) } + case submitPlanNowMsg: + cmds = append(cmds, func() tea.Msg { return hideStartupStatusMsg{} }) case submitPlanFinishedMsg: cmds = append(cmds, func() tea.Msg { return startStartingSnapshotMsg{} }) @@ -243,23 +248,27 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { c.Env = append(c.Env, fmt.Sprintf("AWS_PROFILE=%v", aws_profile)) } - _, span := tracing.Tracer().Start(m.ctx, "terraform apply", trace.WithAttributes( // nolint:spancheck // will be ended in the tea.ExecProcess cleanup func + _, span := tracing.Tracer().Start(m.ctx, "terraform apply", trace.WithAttributes( // nolint:spancheck // will be ended in the tea.Exec cleanup func attribute.String("command", strings.Join(m.args, " ")), )) - return m, tea.ExecProcess( // nolint:spancheck // will be ended in the tea.ExecProcess cleanup func - c, - func(err error) tea.Msg { - defer span.End() - - if err != nil { - return fatalError{err: fmt.Errorf("failed to run terraform apply: %w", err)} - } + return m, tea.Sequence( // nolint:spancheck // will be ended in the tea.Exec cleanup func + func() tea.Msg { return freezeViewMsg{} }, + tea.Exec( + m.execCommandFunc(c), + func(err error) tea.Msg { + defer span.End() + + if err != nil { + return fatalError{err: fmt.Errorf("failed to run terraform apply: %w", err)} + } - return tfApplyFinishedMsg{} - }) + return tfApplyFinishedMsg{} + })) case tfApplyFinishedMsg: m.isEnding = true cmds = append(cmds, + func() tea.Msg { return unfreezeViewMsg{} }, + func() tea.Msg { return hideStartupStatusMsg{} }, m.endingChangeSnapshot.Init(), m.startEndChangeCmd(), m.waitForEndingActivity, diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 6d8e162b..e310c792 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -62,7 +62,7 @@ type mappedItemDiffsMsg struct { unsupported map[string][]*sdp.MappedItemDiff } -func NewTfPlanModel(args []string) tea.Model { +func NewTfPlanModel(args []string, execCommandFunc ExecCommandFunc) tea.Model { hasPlanOutSet := false planFile := "overmind.plan" for i, a := range args { @@ -97,7 +97,7 @@ func NewTfPlanModel(args []string) tea.Model { return tfPlanModel{ args: args, - runPlanTask: NewRunPlanModel(args, planFile), + runPlanTask: NewRunPlanModel(args, planFile, execCommandFunc), submitPlanTask: NewSubmitPlanModel(planFile), planFile: planFile, } @@ -132,6 +132,9 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, func() tea.Msg { return submitPlanNowMsg{} }) } + case submitPlanNowMsg: + cmds = append(cmds, func() tea.Msg { return hideStartupStatusMsg{} }) + case submitPlanFinishedMsg: cmds = append(cmds, func() tea.Msg { return delayQuitMsg{} }) } diff --git a/cmd/theme.go b/cmd/theme.go index 139b51c8..6579978a 100644 --- a/cmd/theme.go +++ b/cmd/theme.go @@ -204,7 +204,7 @@ func MarkdownStyle() ansi.StyleConfig { StylePrimitive: ansi.StylePrimitive{ Italic: ptrBool(true), }, - Indent: ptrUint(2), + Indent: ptrUint(1), IndentToken: ptrString("│ "), }, List: ansi.StyleList{