diff --git a/internal/container/start.go b/internal/container/start.go index 31dae3b5..8604fbe8 100644 --- a/internal/container/start.go +++ b/internal/container/start.go @@ -8,6 +8,7 @@ import ( "os" stdruntime "runtime" "slices" + "strings" "time" "github.com/containerd/errdefs" @@ -121,10 +122,10 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start setups := map[config.EmulatorType]postStartSetupFunc{ config.EmulatorAWS: awsconfig.Setup, } - return runPostStartSetups(ctx, sink, opts.Containers, interactive, opts.LocalStackHost, setups) + return runPostStartSetups(ctx, sink, opts.Containers, interactive, opts.LocalStackHost, opts.WebAppURL, setups) } -func runPostStartSetups(ctx context.Context, sink output.Sink, containers []config.ContainerConfig, interactive bool, localStackHost string, setups map[config.EmulatorType]postStartSetupFunc) error { +func runPostStartSetups(ctx context.Context, sink output.Sink, containers []config.ContainerConfig, interactive bool, localStackHost, webAppURL string, setups map[config.EmulatorType]postStartSetupFunc) error { // build ordered list of unique types, keeping the first container config for each firstByType := map[config.EmulatorType]config.ContainerConfig{} var uniqueEmulatorTypes []config.EmulatorType @@ -143,11 +144,20 @@ func runPostStartSetups(ctx context.Context, sink output.Sink, containers []conf if err := setup(ctx, sink, interactive, resolvedHost); err != nil { return err } + emitPostStartPointers(sink, resolvedHost, webAppURL) } } return nil } +func emitPostStartPointers(sink output.Sink, resolvedHost, webAppURL string) { + output.EmitSecondary(sink, fmt.Sprintf("• Endpoint: %s", resolvedHost)) + if webAppURL != "" { + output.EmitSecondary(sink, fmt.Sprintf("• Web app: %s", strings.TrimRight(webAppURL, "/"))) + } + output.EmitSecondary(sink, "> Tip: View emulator logs: lstk logs --follow") +} + func pullImages(ctx context.Context, rt runtime.Runtime, sink output.Sink, containers []runtime.ContainerConfig) error { for _, c := range containers { // Remove any existing stopped container with the same name diff --git a/internal/container/start_test.go b/internal/container/start_test.go index 9a37f920..42dc4450 100644 --- a/internal/container/start_test.go +++ b/internal/container/start_test.go @@ -1,6 +1,7 @@ package container import ( + "bytes" "context" "errors" "io" @@ -28,3 +29,30 @@ func TestStart_ReturnsEarlyIfRuntimeUnhealthy(t *testing.T) { assert.Contains(t, err.Error(), "runtime not healthy") assert.True(t, output.IsSilent(err), "error should be silent since it was already emitted") } + +func TestEmitPostStartPointers_WithWebApp(t *testing.T) { + var out bytes.Buffer + sink := output.NewPlainSink(&out) + + emitPostStartPointers(sink, "localhost.localstack.cloud:4566", "https://app.localstack.cloud/") + + assert.Equal(t, ""+ + "• Endpoint: localhost.localstack.cloud:4566\n"+ + "• Web app: https://app.localstack.cloud\n"+ + "> Tip: View emulator logs: lstk logs --follow\n", + out.String(), + ) +} + +func TestEmitPostStartPointers_WithoutWebApp(t *testing.T) { + var out bytes.Buffer + sink := output.NewPlainSink(&out) + + emitPostStartPointers(sink, "127.0.0.1:4566", "") + + assert.Equal(t, ""+ + "• Endpoint: 127.0.0.1:4566\n"+ + "> Tip: View emulator logs: lstk logs --follow\n", + out.String(), + ) +} diff --git a/internal/output/events.go b/internal/output/events.go index 563763ea..f73b858e 100644 --- a/internal/output/events.go +++ b/internal/output/events.go @@ -20,10 +20,11 @@ import "time" type MessageSeverity int const ( - SeverityInfo MessageSeverity = iota - SeveritySuccess - SeverityNote - SeverityWarning + SeverityInfo MessageSeverity = iota + SeveritySuccess // positive outcome + SeverityNote // informational + SeverityWarning // cautionary + SeveritySecondary // subdued/decorative text ) type MessageEvent struct { @@ -158,6 +159,10 @@ func EmitWarning(sink Sink, text string) { Emit(sink, MessageEvent{Severity: SeverityWarning, Text: text}) } +func EmitSecondary(sink Sink, text string) { + Emit(sink, MessageEvent{Severity: SeveritySecondary, Text: text}) +} + func EmitStatus(sink Sink, phase, container, detail string) { Emit(sink, ContainerStatusEvent{Phase: phase, Container: container, Detail: detail}) } diff --git a/internal/output/plain_format.go b/internal/output/plain_format.go index 763fa64f..daecb731 100644 --- a/internal/output/plain_format.go +++ b/internal/output/plain_format.go @@ -121,6 +121,8 @@ func formatMessageEvent(e MessageEvent) string { return "> Note: " + e.Text case SeverityWarning: return "> Warning: " + e.Text + case SeveritySecondary: + return e.Text default: return e.Text } diff --git a/internal/ui/components/message.go b/internal/ui/components/message.go index 48d01faf..1b60b6a8 100644 --- a/internal/ui/components/message.go +++ b/internal/ui/components/message.go @@ -15,7 +15,11 @@ func RenderMessage(e output.MessageEvent) string { func RenderWrappedMessage(e output.MessageEvent, width int) string { prefixText, prefix := messagePrefix(e) if prefixText == "" { - return styles.Message.Render(strings.Join(wrap.SoftWrap(e.Text, width), "\n")) + style := styles.Message + if e.Severity == output.SeveritySecondary { + style = styles.SecondaryMessage + } + return style.Render(strings.Join(wrap.SoftWrap(e.Text, width), "\n")) } if width <= len([]rune(prefixText))+1 { diff --git a/internal/ui/components/message_test.go b/internal/ui/components/message_test.go new file mode 100644 index 00000000..e8a86456 --- /dev/null +++ b/internal/ui/components/message_test.go @@ -0,0 +1,31 @@ +package components + +import ( + "testing" + + "github.com/localstack/lstk/internal/output" + "github.com/localstack/lstk/internal/ui/styles" + "github.com/stretchr/testify/assert" +) + +func TestRenderMessage_SecondaryUsesSubduedStyle(t *testing.T) { + tests := []string{ + "• Endpoint: localhost.localstack.cloud:4566", + "• Web app: https://app.localstack.cloud", + "> Tip: View emulator logs: lstk logs --follow", + } + + for _, text := range tests { + assert.Equal(t, styles.SecondaryMessage.Render(text), RenderMessage(output.MessageEvent{ + Severity: output.SeveritySecondary, + Text: text, + })) + } +} + +func TestRenderMessage_LeavesRegularInfoLinesUnchanged(t *testing.T) { + assert.Equal(t, styles.Message.Render("hello"), RenderMessage(output.MessageEvent{ + Severity: output.SeverityInfo, + Text: "hello", + })) +}