From dcc912715bab60e9c7c564309d16a436d5cb85bb Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Fri, 13 Mar 2026 15:05:35 +0200 Subject: [PATCH 1/5] Use checkmark for success message --- internal/output/plain_format.go | 6 +++--- internal/output/plain_format_test.go | 4 ++-- internal/output/plain_sink_test.go | 4 ++-- internal/output/style.go | 13 +++++++++++++ internal/ui/app_test.go | 2 +- internal/ui/components/message.go | 3 ++- internal/ui/styles/styles.go | 7 +++++-- 7 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 internal/output/style.go diff --git a/internal/output/plain_format.go b/internal/output/plain_format.go index daecb731..79d32782 100644 --- a/internal/output/plain_format.go +++ b/internal/output/plain_format.go @@ -49,9 +49,9 @@ func formatStatusLine(e ContainerStatusEvent) (string, bool) { return "Waiting for LocalStack to be ready...", true case "ready": if e.Detail != "" { - return fmt.Sprintf("LocalStack ready (%s)", e.Detail), true + return fmt.Sprintf("%s LocalStack ready (%s)", SuccessMarkerText(), e.Detail), true } - return "LocalStack ready", true + return SuccessMarkerText() + " LocalStack ready", true default: if e.Detail != "" { return fmt.Sprintf("LocalStack: %s (%s)", e.Phase, e.Detail), true @@ -116,7 +116,7 @@ func formatAuthEvent(e AuthEvent) string { func formatMessageEvent(e MessageEvent) string { switch e.Severity { case SeveritySuccess: - return "> Success: " + e.Text + return SuccessMarkerText() + " " + e.Text case SeverityNote: return "> Note: " + e.Text case SeverityWarning: diff --git a/internal/output/plain_format_test.go b/internal/output/plain_format_test.go index a508e7d9..187d03e1 100644 --- a/internal/output/plain_format_test.go +++ b/internal/output/plain_format_test.go @@ -24,7 +24,7 @@ func TestFormatEventLine(t *testing.T) { { name: "message event success", event: MessageEvent{Severity: SeveritySuccess, Text: "done"}, - want: "> Success: done", + want: SuccessMarkerText() + " done", wantOK: true, }, { @@ -60,7 +60,7 @@ func TestFormatEventLine(t *testing.T) { { name: "status ready with detail", event: ContainerStatusEvent{Phase: "ready", Container: "localstack-aws", Detail: "abc123"}, - want: "LocalStack ready (abc123)", + want: SuccessMarkerText() + " LocalStack ready (abc123)", wantOK: true, }, { diff --git a/internal/output/plain_sink_test.go b/internal/output/plain_sink_test.go index 38dcf8c2..ee9a2755 100644 --- a/internal/output/plain_sink_test.go +++ b/internal/output/plain_sink_test.go @@ -61,12 +61,12 @@ func TestPlainSink_EmitsStatusEvent(t *testing.T) { { name: "ready phase with detail", event: ContainerStatusEvent{Phase: "ready", Container: "localstack-aws", Detail: "abc123"}, - expected: "LocalStack ready (abc123)\n", + expected: fmt.Sprintf("%s LocalStack ready (abc123)\n", SuccessMarkerText()), }, { name: "ready phase without detail", event: ContainerStatusEvent{Phase: "ready", Container: "localstack-aws"}, - expected: "LocalStack ready\n", + expected: fmt.Sprintf("%s LocalStack ready\n", SuccessMarkerText()), }, { name: "unknown phase with detail", diff --git a/internal/output/style.go b/internal/output/style.go new file mode 100644 index 00000000..a24a5e97 --- /dev/null +++ b/internal/output/style.go @@ -0,0 +1,13 @@ +package output + +import "fmt" + +const SuccessColorHex = "#B7C95C" + +func SuccessMarker() string { + return fmt.Sprintf("\x1b[38;2;183;201;92m%s\x1b[0m", SuccessMarkerText()) +} + +func SuccessMarkerText() string { + return "✔︎" +} diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index 0949fa26..06a3d20e 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -176,7 +176,7 @@ func TestAppMessageEventRendering(t *testing.T) { if len(app.lines) != 1 { t.Fatalf("expected 1 line, got %d", len(app.lines)) } - if !strings.Contains(app.lines[0].text, "Success:") || !strings.Contains(app.lines[0].text, "Done") { + if !strings.Contains(app.lines[0].text, output.SuccessMarkerText()) || !strings.Contains(app.lines[0].text, "Done") { t.Fatalf("expected rendered success message, got: %q", app.lines[0].text) } } diff --git a/internal/ui/components/message.go b/internal/ui/components/message.go index 1b60b6a8..88920f3b 100644 --- a/internal/ui/components/message.go +++ b/internal/ui/components/message.go @@ -45,7 +45,8 @@ func messagePrefix(e output.MessageEvent) (string, string) { prefix := styles.Secondary.Render("> ") switch e.Severity { case output.SeveritySuccess: - return "> Success:", prefix + styles.Success.Render("Success:") + checkmark := output.SuccessMarkerText() + return "> " + checkmark, prefix + styles.Success.Render(checkmark) case output.SeverityNote: return "> Note:", prefix + styles.Note.Render("Note:") case output.SeverityWarning: diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index c32e9867..38330e12 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -1,6 +1,9 @@ package styles -import "github.com/charmbracelet/lipgloss" +import ( + "github.com/charmbracelet/lipgloss" + "github.com/localstack/lstk/internal/output" +) const ( NimboDarkColor = "#3F51C7" @@ -39,7 +42,7 @@ var ( // Message severity styles Success = lipgloss.NewStyle(). - Foreground(lipgloss.Color("42")) + Foreground(lipgloss.Color(output.SuccessColorHex)) Note = lipgloss.NewStyle(). Foreground(lipgloss.Color("33")) From 538e88ed54c2410e51bf1d07e334f12c55d028b7 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Mon, 16 Mar 2026 22:00:32 +0200 Subject: [PATCH 2/5] Remove raw ANSI color from non-TUI success marker --- internal/output/style.go | 6 ------ internal/ui/app.go | 3 +++ internal/ui/components/message.go | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/output/style.go b/internal/output/style.go index a24a5e97..33e1f39c 100644 --- a/internal/output/style.go +++ b/internal/output/style.go @@ -1,13 +1,7 @@ package output -import "fmt" - const SuccessColorHex = "#B7C95C" -func SuccessMarker() string { - return fmt.Sprintf("\x1b[38;2;183;201;92m%s\x1b[0m", SuccessMarkerText()) -} - func SuccessMarkerText() string { return "✔︎" } diff --git a/internal/ui/app.go b/internal/ui/app.go index 3dc7fe8b..7d565b90 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -182,6 +182,9 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.pullProgress = a.pullProgress.Hide() } if line, ok := output.FormatEventLine(msg); ok { + if msg.Phase == "ready" { + line = strings.Replace(line, output.SuccessMarkerText(), styles.Success.Render(output.SuccessMarkerText()), 1) + } a.lines = appendLine(a.lines, styledLine{text: line}) } return a, nil diff --git a/internal/ui/components/message.go b/internal/ui/components/message.go index 88920f3b..f2d36abd 100644 --- a/internal/ui/components/message.go +++ b/internal/ui/components/message.go @@ -46,7 +46,7 @@ func messagePrefix(e output.MessageEvent) (string, string) { switch e.Severity { case output.SeveritySuccess: checkmark := output.SuccessMarkerText() - return "> " + checkmark, prefix + styles.Success.Render(checkmark) + return checkmark, styles.Success.Render(checkmark) case output.SeverityNote: return "> Note:", prefix + styles.Note.Render("Note:") case output.SeverityWarning: From 321c655c38c520e4efb15b6f0c2a3de530b1f031 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Tue, 17 Mar 2026 10:46:20 +0200 Subject: [PATCH 3/5] Use SuccessMarkerText for instance info output --- internal/output/plain_format.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/output/plain_format.go b/internal/output/plain_format.go index 79d32782..251b325c 100644 --- a/internal/output/plain_format.go +++ b/internal/output/plain_format.go @@ -151,7 +151,7 @@ func formatErrorEvent(e ErrorEvent) string { func formatInstanceInfo(e InstanceInfoEvent) string { var sb strings.Builder - sb.WriteString("✓ " + e.EmulatorName + " is running (" + e.Host + ")") + sb.WriteString(SuccessMarkerText() + " " + e.EmulatorName + " is running (" + e.Host + ")") var meta []string if e.Uptime > 0 { meta = append(meta, "UPTIME: "+formatUptime(e.Uptime)) From 873be544eb18ac4f3ecfd72b0b4dfe6b0a9bb2bc Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Tue, 17 Mar 2026 17:51:15 +0200 Subject: [PATCH 4/5] Use SuccessMarkerText() in instance info test expectations --- internal/output/plain_format_test.go | 4 ++-- internal/output/plain_sink_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/output/plain_format_test.go b/internal/output/plain_format_test.go index 187d03e1..eaa9e898 100644 --- a/internal/output/plain_format_test.go +++ b/internal/output/plain_format_test.go @@ -120,7 +120,7 @@ func TestFormatEventLine(t *testing.T) { ContainerName: "localstack-aws", Uptime: 4*time.Minute + 23*time.Second, }, - want: "✓ LocalStack AWS Emulator is running (localhost.localstack.cloud:4566)\n UPTIME: 4m 23s · CONTAINER: localstack-aws · VERSION: 4.14.1", + want: SuccessMarkerText() + " LocalStack AWS Emulator is running (localhost.localstack.cloud:4566)\n UPTIME: 4m 23s · CONTAINER: localstack-aws · VERSION: 4.14.1", wantOK: true, }, { @@ -129,7 +129,7 @@ func TestFormatEventLine(t *testing.T) { EmulatorName: "LocalStack AWS Emulator", Host: "127.0.0.1:4566", }, - want: "✓ LocalStack AWS Emulator is running (127.0.0.1:4566)", + want: SuccessMarkerText() + " LocalStack AWS Emulator is running (127.0.0.1:4566)", wantOK: true, }, { diff --git a/internal/output/plain_sink_test.go b/internal/output/plain_sink_test.go index ee9a2755..f26d7d40 100644 --- a/internal/output/plain_sink_test.go +++ b/internal/output/plain_sink_test.go @@ -163,7 +163,7 @@ func TestPlainSink_EmitsInstanceInfoEvent(t *testing.T) { Uptime: 4*time.Minute + 23*time.Second, }) - expected := "✓ LocalStack AWS Emulator is running (localhost.localstack.cloud:4566)\n UPTIME: 4m 23s · CONTAINER: localstack-aws · VERSION: 4.14.1\n" + expected := SuccessMarkerText() + " LocalStack AWS Emulator is running (localhost.localstack.cloud:4566)\n UPTIME: 4m 23s · CONTAINER: localstack-aws · VERSION: 4.14.1\n" assert.Equal(t, expected, out.String()) assert.NoError(t, sink.Err()) }) @@ -177,7 +177,7 @@ func TestPlainSink_EmitsInstanceInfoEvent(t *testing.T) { Host: "127.0.0.1:4566", }) - expected := "✓ LocalStack AWS Emulator is running (127.0.0.1:4566)\n" + expected := SuccessMarkerText() + " LocalStack AWS Emulator is running (127.0.0.1:4566)\n" assert.Equal(t, expected, out.String()) assert.NoError(t, sink.Err()) }) From 44f3419da11b5747b6febead24777c2b7be33f2e Mon Sep 17 00:00:00 2001 From: Silvio Vasiljevic Date: Fri, 20 Mar 2026 18:25:42 +0200 Subject: [PATCH 5/5] Split styles and content for success marker and move to respective locations --- internal/output/plain_format.go | 8 ++++---- internal/output/plain_format_test.go | 9 ++++----- internal/output/plain_sink_test.go | 8 ++++---- internal/output/style.go | 7 ------- internal/output/symbols.go | 5 +++++ internal/ui/app.go | 2 +- internal/ui/app_test.go | 2 +- internal/ui/components/message.go | 2 +- internal/ui/styles/styles.go | 4 ++-- 9 files changed, 22 insertions(+), 25 deletions(-) delete mode 100644 internal/output/style.go create mode 100644 internal/output/symbols.go diff --git a/internal/output/plain_format.go b/internal/output/plain_format.go index 251b325c..2c1c0a84 100644 --- a/internal/output/plain_format.go +++ b/internal/output/plain_format.go @@ -49,9 +49,9 @@ func formatStatusLine(e ContainerStatusEvent) (string, bool) { return "Waiting for LocalStack to be ready...", true case "ready": if e.Detail != "" { - return fmt.Sprintf("%s LocalStack ready (%s)", SuccessMarkerText(), e.Detail), true + return fmt.Sprintf("%s LocalStack ready (%s)", SuccessMarker(), e.Detail), true } - return SuccessMarkerText() + " LocalStack ready", true + return SuccessMarker() + " LocalStack ready", true default: if e.Detail != "" { return fmt.Sprintf("LocalStack: %s (%s)", e.Phase, e.Detail), true @@ -116,7 +116,7 @@ func formatAuthEvent(e AuthEvent) string { func formatMessageEvent(e MessageEvent) string { switch e.Severity { case SeveritySuccess: - return SuccessMarkerText() + " " + e.Text + return SuccessMarker() + " " + e.Text case SeverityNote: return "> Note: " + e.Text case SeverityWarning: @@ -151,7 +151,7 @@ func formatErrorEvent(e ErrorEvent) string { func formatInstanceInfo(e InstanceInfoEvent) string { var sb strings.Builder - sb.WriteString(SuccessMarkerText() + " " + e.EmulatorName + " is running (" + e.Host + ")") + sb.WriteString(SuccessMarker() + " " + e.EmulatorName + " is running (" + e.Host + ")") var meta []string if e.Uptime > 0 { meta = append(meta, "UPTIME: "+formatUptime(e.Uptime)) diff --git a/internal/output/plain_format_test.go b/internal/output/plain_format_test.go index eaa9e898..84fcb8ba 100644 --- a/internal/output/plain_format_test.go +++ b/internal/output/plain_format_test.go @@ -24,7 +24,7 @@ func TestFormatEventLine(t *testing.T) { { name: "message event success", event: MessageEvent{Severity: SeveritySuccess, Text: "done"}, - want: SuccessMarkerText() + " done", + want: SuccessMarker() + " done", wantOK: true, }, { @@ -60,7 +60,7 @@ func TestFormatEventLine(t *testing.T) { { name: "status ready with detail", event: ContainerStatusEvent{Phase: "ready", Container: "localstack-aws", Detail: "abc123"}, - want: SuccessMarkerText() + " LocalStack ready (abc123)", + want: SuccessMarker() + " LocalStack ready (abc123)", wantOK: true, }, { @@ -120,7 +120,7 @@ func TestFormatEventLine(t *testing.T) { ContainerName: "localstack-aws", Uptime: 4*time.Minute + 23*time.Second, }, - want: SuccessMarkerText() + " LocalStack AWS Emulator is running (localhost.localstack.cloud:4566)\n UPTIME: 4m 23s · CONTAINER: localstack-aws · VERSION: 4.14.1", + want: SuccessMarker() + " LocalStack AWS Emulator is running (localhost.localstack.cloud:4566)\n UPTIME: 4m 23s · CONTAINER: localstack-aws · VERSION: 4.14.1", wantOK: true, }, { @@ -129,7 +129,7 @@ func TestFormatEventLine(t *testing.T) { EmulatorName: "LocalStack AWS Emulator", Host: "127.0.0.1:4566", }, - want: SuccessMarkerText() + " LocalStack AWS Emulator is running (127.0.0.1:4566)", + want: SuccessMarker() + " LocalStack AWS Emulator is running (127.0.0.1:4566)", wantOK: true, }, { @@ -237,4 +237,3 @@ func TestFormatTableWidth(t *testing.T) { } }) } - diff --git a/internal/output/plain_sink_test.go b/internal/output/plain_sink_test.go index f26d7d40..f6908d94 100644 --- a/internal/output/plain_sink_test.go +++ b/internal/output/plain_sink_test.go @@ -61,12 +61,12 @@ func TestPlainSink_EmitsStatusEvent(t *testing.T) { { name: "ready phase with detail", event: ContainerStatusEvent{Phase: "ready", Container: "localstack-aws", Detail: "abc123"}, - expected: fmt.Sprintf("%s LocalStack ready (abc123)\n", SuccessMarkerText()), + expected: fmt.Sprintf("%s LocalStack ready (abc123)\n", SuccessMarker()), }, { name: "ready phase without detail", event: ContainerStatusEvent{Phase: "ready", Container: "localstack-aws"}, - expected: fmt.Sprintf("%s LocalStack ready\n", SuccessMarkerText()), + expected: fmt.Sprintf("%s LocalStack ready\n", SuccessMarker()), }, { name: "unknown phase with detail", @@ -163,7 +163,7 @@ func TestPlainSink_EmitsInstanceInfoEvent(t *testing.T) { Uptime: 4*time.Minute + 23*time.Second, }) - expected := SuccessMarkerText() + " LocalStack AWS Emulator is running (localhost.localstack.cloud:4566)\n UPTIME: 4m 23s · CONTAINER: localstack-aws · VERSION: 4.14.1\n" + expected := SuccessMarker() + " LocalStack AWS Emulator is running (localhost.localstack.cloud:4566)\n UPTIME: 4m 23s · CONTAINER: localstack-aws · VERSION: 4.14.1\n" assert.Equal(t, expected, out.String()) assert.NoError(t, sink.Err()) }) @@ -177,7 +177,7 @@ func TestPlainSink_EmitsInstanceInfoEvent(t *testing.T) { Host: "127.0.0.1:4566", }) - expected := SuccessMarkerText() + " LocalStack AWS Emulator is running (127.0.0.1:4566)\n" + expected := SuccessMarker() + " LocalStack AWS Emulator is running (127.0.0.1:4566)\n" assert.Equal(t, expected, out.String()) assert.NoError(t, sink.Err()) }) diff --git a/internal/output/style.go b/internal/output/style.go deleted file mode 100644 index 33e1f39c..00000000 --- a/internal/output/style.go +++ /dev/null @@ -1,7 +0,0 @@ -package output - -const SuccessColorHex = "#B7C95C" - -func SuccessMarkerText() string { - return "✔︎" -} diff --git a/internal/output/symbols.go b/internal/output/symbols.go new file mode 100644 index 00000000..94834287 --- /dev/null +++ b/internal/output/symbols.go @@ -0,0 +1,5 @@ +package output + +func SuccessMarker() string { + return "✔︎" +} diff --git a/internal/ui/app.go b/internal/ui/app.go index 7d565b90..4185bd3b 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -183,7 +183,7 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if line, ok := output.FormatEventLine(msg); ok { if msg.Phase == "ready" { - line = strings.Replace(line, output.SuccessMarkerText(), styles.Success.Render(output.SuccessMarkerText()), 1) + line = strings.Replace(line, output.SuccessMarker(), styles.Success.Render(output.SuccessMarker()), 1) } a.lines = appendLine(a.lines, styledLine{text: line}) } diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index 06a3d20e..b2da93d8 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -176,7 +176,7 @@ func TestAppMessageEventRendering(t *testing.T) { if len(app.lines) != 1 { t.Fatalf("expected 1 line, got %d", len(app.lines)) } - if !strings.Contains(app.lines[0].text, output.SuccessMarkerText()) || !strings.Contains(app.lines[0].text, "Done") { + if !strings.Contains(app.lines[0].text, output.SuccessMarker()) || !strings.Contains(app.lines[0].text, "Done") { t.Fatalf("expected rendered success message, got: %q", app.lines[0].text) } } diff --git a/internal/ui/components/message.go b/internal/ui/components/message.go index f2d36abd..4bf19e96 100644 --- a/internal/ui/components/message.go +++ b/internal/ui/components/message.go @@ -45,7 +45,7 @@ func messagePrefix(e output.MessageEvent) (string, string) { prefix := styles.Secondary.Render("> ") switch e.Severity { case output.SeveritySuccess: - checkmark := output.SuccessMarkerText() + checkmark := output.SuccessMarker() return checkmark, styles.Success.Render(checkmark) case output.SeverityNote: return "> Note:", prefix + styles.Note.Render("Note:") diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index 38330e12..a3daa4b3 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -2,13 +2,13 @@ package styles import ( "github.com/charmbracelet/lipgloss" - "github.com/localstack/lstk/internal/output" ) const ( NimboDarkColor = "#3F51C7" NimboMidColor = "#5E6AD2" NimboLightColor = "#7E88EC" + SuccessColor = "#B7C95C" ) var ( @@ -42,7 +42,7 @@ var ( // Message severity styles Success = lipgloss.NewStyle(). - Foreground(lipgloss.Color(output.SuccessColorHex)) + Foreground(lipgloss.Color(SuccessColor)) Note = lipgloss.NewStyle(). Foreground(lipgloss.Color("33"))