Skip to content

Commit e587314

Browse files
committed
Make start header more compact with spinner-based status lines
1 parent b2178cd commit e587314

6 files changed

Lines changed: 32 additions & 10 deletions

File tree

internal/container/start.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,9 +366,10 @@ func validateLicensesFromImages(ctx context.Context, rt runtime.Runtime, sink ou
366366
func startContainers(ctx context.Context, rt runtime.Runtime, sink output.Sink, tel *telemetry.Client, containers []runtime.ContainerConfig, pulled map[string]bool) error {
367367
for _, c := range containers {
368368
startTime := time.Now()
369-
sink.Emit(output.ContainerStatusEvent{Phase: "starting", Container: c.Name})
369+
sink.Emit(output.SpinnerStart("Starting LocalStack"))
370370
containerID, err := rt.Start(ctx, c)
371371
if err != nil {
372+
sink.Emit(output.SpinnerStop())
372373
tel.EmitEmulatorLifecycleEvent(ctx, telemetry.LifecycleEvent{
373374
EventType: telemetry.LifecycleStartError,
374375
Emulator: c.EmulatorType,
@@ -379,9 +380,9 @@ func startContainers(ctx context.Context, rt runtime.Runtime, sink output.Sink,
379380
return fmt.Errorf("failed to start LocalStack: %w", err)
380381
}
381382

382-
sink.Emit(output.ContainerStatusEvent{Phase: "waiting", Container: c.Name})
383383
healthURL := fmt.Sprintf("http://localhost:%s%s", c.Port, c.HealthPath)
384384
if err := awaitStartup(ctx, rt, sink, containerID, "LocalStack", healthURL); err != nil {
385+
sink.Emit(output.SpinnerStop())
385386
errCode := telemetry.ErrCodeStartFailed
386387
var licErr *licenseNotCoveredError
387388
if errors.As(err, &licErr) && c.EmulatorType == config.EmulatorSnowflake {
@@ -404,6 +405,7 @@ func startContainers(ctx context.Context, rt runtime.Runtime, sink output.Sink,
404405
})
405406
return err
406407
}
408+
sink.Emit(output.SpinnerStop())
407409

408410
sink.Emit(output.ContainerStatusEvent{Phase: "ready", Container: c.Name, Detail: fmt.Sprintf("containerId: %s", containerID[:12])})
409411

@@ -549,7 +551,7 @@ func emitPortInUseError(sink output.Sink, port string) {
549551

550552
func validateLicense(ctx context.Context, sink output.Sink, opts StartOptions, containerConfig runtime.ContainerConfig, token, licenseFilePath string) error {
551553
version := containerConfig.Tag
552-
sink.Emit(output.ContainerStatusEvent{Phase: "validating license", Container: containerConfig.Name})
554+
sink.Emit(output.SpinnerStart("Checking license"))
553555

554556
hostname, _ := os.Hostname()
555557
licenseReq := &api.LicenseRequest{
@@ -569,6 +571,7 @@ func validateLicense(ctx context.Context, sink output.Sink, opts StartOptions, c
569571

570572
licenseResp, err := opts.PlatformClient.GetLicense(ctx, licenseReq)
571573
if err != nil {
574+
sink.Emit(output.SpinnerStop())
572575
var licErr *api.LicenseError
573576
if errors.As(err, &licErr) && licErr.Detail != "" {
574577
opts.Logger.Error("license server response (HTTP %d): %s", licErr.Status, licErr.Detail)
@@ -582,6 +585,13 @@ func validateLicense(ctx context.Context, sink output.Sink, opts StartOptions, c
582585
})
583586
return fmt.Errorf("license validation failed for %s:%s: %w", containerConfig.ProductName, version, err)
584587
}
588+
sink.Emit(output.SpinnerStop())
589+
590+
validMsg := "Valid license"
591+
if plan := licenseResp.PlanDisplayName(); plan != "" {
592+
validMsg = fmt.Sprintf("Valid license (%s)", plan)
593+
}
594+
sink.Emit(output.MessageEvent{Severity: output.SeveritySuccess, Text: validMsg})
585595

586596
if licenseResp != nil && len(licenseResp.RawBytes) > 0 {
587597
if err := os.MkdirAll(filepath.Dir(licenseFilePath), 0755); err != nil {

internal/output/plain_format.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ func formatStatusLine(e ContainerStatusEvent) (string, bool) {
5151
return "Waiting for LocalStack to be ready...", true
5252
case "ready":
5353
if e.Detail != "" {
54-
return fmt.Sprintf("%s LocalStack ready (%s)", SuccessMarker(), e.Detail), true
54+
return fmt.Sprintf("%s LocalStack is running (%s)", SuccessMarker(), e.Detail), true
5555
}
56-
return SuccessMarker() + " LocalStack ready", true
56+
return SuccessMarker() + " LocalStack is running", true
5757
default:
5858
if e.Detail != "" {
5959
return fmt.Sprintf("LocalStack: %s (%s)", e.Phase, e.Detail), true

internal/output/plain_format_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func TestFormatEventLine(t *testing.T) {
6060
{
6161
name: "status ready with detail",
6262
event: ContainerStatusEvent{Phase: "ready", Container: "localstack-aws", Detail: "abc123"},
63-
want: SuccessMarker() + " LocalStack ready (abc123)",
63+
want: SuccessMarker() + " LocalStack is running (abc123)",
6464
wantOK: true,
6565
},
6666
{

internal/output/plain_sink_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ func TestPlainSink_EmitsStatusEvent(t *testing.T) {
6161
{
6262
name: "ready phase with detail",
6363
event: ContainerStatusEvent{Phase: "ready", Container: "localstack-aws", Detail: "abc123"},
64-
expected: fmt.Sprintf("%s LocalStack ready (abc123)\n", SuccessMarker()),
64+
expected: fmt.Sprintf("%s LocalStack is running (abc123)\n", SuccessMarker()),
6565
},
6666
{
6767
name: "ready phase without detail",
6868
event: ContainerStatusEvent{Phase: "ready", Container: "localstack-aws"},
69-
expected: fmt.Sprintf("%s LocalStack ready\n", SuccessMarker()),
69+
expected: fmt.Sprintf("%s LocalStack is running\n", SuccessMarker()),
7070
},
7171
{
7272
name: "unknown phase with detail",

internal/ui/app.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
187187
a.spinner = a.spinner.Start(msg.Text, msg.MinDuration)
188188
return a, a.spinner.Tick()
189189
}
190+
if a.pullProgress.Visible() {
191+
a.pullProgress = a.pullProgress.Hide()
192+
}
190193
var cmd tea.Cmd
191194
a.spinner, cmd = a.spinner.Stop()
192195
if !a.spinner.PendingStop() {
@@ -209,7 +212,7 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
209212
a.headerLoading = false
210213
}
211214
a.errorDisplay = a.errorDisplay.Show(msg)
212-
a.spinner, _ = a.spinner.Stop()
215+
a.spinner = a.spinner.ForceStop()
213216
return a, nil
214217
case output.MessageEvent:
215218
msgCopy := msg
@@ -267,7 +270,7 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
267270
return a, tea.Quit
268271
case runErrMsg:
269272
a.err = msg.err
270-
a.spinner, _ = a.spinner.Stop()
273+
a.spinner = a.spinner.ForceStop()
271274
a.flushBufferedLines()
272275
if !output.IsSilent(msg.err) {
273276
a.errorDisplay = a.errorDisplay.Show(output.ErrorEvent{Title: msg.err.Error()})

internal/ui/components/spinner.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ func (s Spinner) Stop() (Spinner, tea.Cmd) {
5151
})
5252
}
5353

54+
// ForceStop clears the spinner immediately, ignoring the min-duration smoothing
55+
// that Stop applies. Use this on error or terminal paths where a soft stop would
56+
// leave a stale frame on the final render.
57+
func (s Spinner) ForceStop() Spinner {
58+
s.visible = false
59+
s.pendingStop = false
60+
return s
61+
}
62+
5463
func (s Spinner) PendingStop() bool {
5564
return s.pendingStop
5665
}

0 commit comments

Comments
 (0)