diff --git a/internal/config/containers.go b/internal/config/containers.go index 7903b26..5649dcf 100644 --- a/internal/config/containers.go +++ b/internal/config/containers.go @@ -14,6 +14,12 @@ const ( userConfigFileName = "config.toml" ) +var emulatorDisplayNames = map[EmulatorType]string{ + EmulatorAWS: "AWS", + EmulatorSnowflake: "Snowflake", + EmulatorAzure: "Azure", +} + var emulatorImages = map[EmulatorType]string{ EmulatorAWS: "localstack-pro", } @@ -58,6 +64,14 @@ func (c *ContainerConfig) HealthPath() (string, error) { return path, nil } +func (c *ContainerConfig) DisplayName() string { + name, ok := emulatorDisplayNames[c.Type] + if !ok { + return fmt.Sprintf("LocalStack %s Emulator", c.Type) + } + return fmt.Sprintf("LocalStack %s Emulator", name) +} + func (c *ContainerConfig) ProductName() (string, error) { productName, ok := emulatorImages[c.Type] if !ok { diff --git a/internal/ui/app.go b/internal/ui/app.go index 48e32a4..deaa664 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -31,9 +31,9 @@ type App struct { quitting bool } -func NewApp(version string, cancel func()) App { +func NewApp(version, emulatorName, endpoint string, cancel func()) App { return App{ - header: components.NewHeader(version), + header: components.NewHeader(version, emulatorName, endpoint), inputPrompt: components.NewInputPrompt(), spinner: components.NewSpinner(), errorDisplay: components.NewErrorDisplay(), diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index 27b009e..9a49ab9 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -12,7 +12,7 @@ import ( ) func TestAppAddsFormattedLinesInOrder(t *testing.T) { - tm := teatest.NewTestModel(t, NewApp("dev", nil), teatest.WithInitialTermSize(120, 40)) + tm := teatest.NewTestModel(t, NewApp("dev", "", "", nil), teatest.WithInitialTermSize(120, 40)) tm.Send(output.MessageEvent{Severity: output.SeverityInfo, Text: "first"}) tm.Send(output.MessageEvent{Severity: output.SeverityWarning, Text: "second"}) @@ -31,7 +31,7 @@ func TestAppAddsFormattedLinesInOrder(t *testing.T) { func TestAppBoundsMessageHistory(t *testing.T) { t.Parallel() - app := NewApp("dev", nil) + app := NewApp("dev", "", "", nil) for i := 0; i < maxLines+5; i++ { model, _ := app.Update(output.MessageEvent{Severity: output.SeverityInfo, Text: "line"}) app = model.(App) @@ -45,7 +45,7 @@ func TestAppQuitCancelsContext(t *testing.T) { t.Parallel() cancelled := false - app := NewApp("dev", func() { cancelled = true }) + app := NewApp("dev", "", "", func() { cancelled = true }) model, cmd := app.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}}) app = model.(App) @@ -63,7 +63,7 @@ func TestAppQuitCancelsContext(t *testing.T) { func TestAppEnterRespondsToInputRequest(t *testing.T) { t.Parallel() - app := NewApp("dev", nil) + app := NewApp("dev", "", "", nil) // First, send a user input request responseCh := make(chan output.InputResponse, 1) @@ -107,7 +107,7 @@ func TestAppCtrlCCancelsPendingInput(t *testing.T) { t.Parallel() cancelled := false - app := NewApp("dev", func() { cancelled = true }) + app := NewApp("dev", "", "", func() { cancelled = true }) // Send a user input request responseCh := make(chan output.InputResponse, 1) @@ -146,7 +146,7 @@ func TestAppCtrlCCancelsPendingInput(t *testing.T) { func TestAppSpinnerStartStop(t *testing.T) { t.Parallel() - app := NewApp("dev", nil) + app := NewApp("dev", "", "", nil) if app.spinner.Visible() { t.Fatal("expected spinner to be hidden initially") @@ -173,7 +173,7 @@ func TestAppSpinnerStartStop(t *testing.T) { func TestAppMessageEventRendering(t *testing.T) { t.Parallel() - app := NewApp("dev", nil) + app := NewApp("dev", "", "", nil) model, _ := app.Update(output.MessageEvent{Severity: output.SeveritySuccess, Text: "Done"}) app = model.(App) @@ -189,7 +189,7 @@ func TestAppMessageEventRendering(t *testing.T) { func TestAppErrorEventStopsSpinner(t *testing.T) { t.Parallel() - app := NewApp("dev", nil) + app := NewApp("dev", "", "", nil) model, _ := app.Update(output.SpinnerEvent{Active: true, Text: "Loading"}) app = model.(App) diff --git a/internal/ui/components/header.go b/internal/ui/components/header.go index 6996f58..d5a14bb 100644 --- a/internal/ui/components/header.go +++ b/internal/ui/components/header.go @@ -28,11 +28,13 @@ func nimboLine3() string { } type Header struct { - version string + version string + emulatorName string + endpoint string } -func NewHeader(version string) Header { - return Header{version: version} +func NewHeader(version, emulatorName, endpoint string) Header { + return Header{version: version, emulatorName: emulatorName, endpoint: endpoint} } func (h Header) View() string { @@ -45,12 +47,17 @@ func (h Header) View() string { )) text := lipgloss.JoinVertical(lipgloss.Left, - styles.Title.Render("LocalStack (lstk)"), - styles.Version.Render(h.version), - "", + "lstk " + styles.Secondary.Render("("+h.version+")"), + styles.Secondary.Render(h.emulatorName), + styles.Secondary.Render(h.endpoint), ) spacer := strings.Repeat(" ", headerPadding) - return "\n" + lipgloss.JoinHorizontal(lipgloss.Top, nimbo, spacer, text) + "\n" + joined := lipgloss.JoinHorizontal(lipgloss.Top, nimbo, spacer, text) + lines := strings.Split(joined, "\n") + for i, line := range lines { + lines[i] = strings.TrimRight(line, " ") + } + return "\n" + strings.Join(lines, "\n") + "\n" } diff --git a/internal/ui/components/header_test.go b/internal/ui/components/header_test.go index abfc682..c65fcde 100644 --- a/internal/ui/components/header_test.go +++ b/internal/ui/components/header_test.go @@ -13,6 +13,6 @@ func TestHeaderView(t *testing.T) { lipgloss.SetColorProfile(termenv.TrueColor) t.Cleanup(func() { lipgloss.SetColorProfile(original) }) - view := NewHeader("v1.0.0").View() + view := NewHeader("v1.0.0", "LocalStack AWS Emulator", "localhost.localstack.cloud:4566").View() golden.Assert(t, view, "header.golden") } diff --git a/internal/ui/components/testdata/header.golden b/internal/ui/components/testdata/header.golden index 68734b0..b0ab5e7 100644 --- a/internal/ui/components/testdata/header.golden +++ b/internal/ui/components/testdata/header.golden @@ -1,4 +1,4 @@ - ▟████▖ LocalStack (lstk) - ▟██▙█▙█▟ v1.0.0 - ▀▛▀▛▀ + ▟████▖ lstk (v1.0.0) + ▟██▙█▙█▟ LocalStack AWS Emulator + ▀▛▀▛▀ localhost.localstack.cloud:4566 diff --git a/internal/ui/run.go b/internal/ui/run.go index c2c5be6..b952cc7 100644 --- a/internal/ui/run.go +++ b/internal/ui/run.go @@ -3,10 +3,12 @@ package ui import ( "context" "errors" + "fmt" "os" tea "github.com/charmbracelet/bubbletea" "github.com/localstack/lstk/internal/api" + "github.com/localstack/lstk/internal/config" "github.com/localstack/lstk/internal/container" "github.com/localstack/lstk/internal/output" "github.com/localstack/lstk/internal/runtime" @@ -28,7 +30,17 @@ func Run(parentCtx context.Context, rt runtime.Runtime, version string, platform ctx, cancel := context.WithCancel(parentCtx) defer cancel() - app := NewApp(version, cancel) + // FIXME: This assumes a single emulator; revisit for proper multi-emulator support + emulatorName := "LocalStack Emulator" + endpoint := "localhost.localstack.cloud" + if cfg, err := config.Get(); err == nil && len(cfg.Containers) > 0 { + emulatorName = cfg.Containers[0].DisplayName() + if cfg.Containers[0].Port != "" { + endpoint = fmt.Sprintf("localhost.localstack.cloud:%s", cfg.Containers[0].Port) + } + } + + app := NewApp(version, emulatorName, endpoint, cancel) p := tea.NewProgram(app) runErrCh := make(chan error, 1) diff --git a/internal/ui/run_login.go b/internal/ui/run_login.go index 2f53016..834f629 100644 --- a/internal/ui/run_login.go +++ b/internal/ui/run_login.go @@ -15,7 +15,7 @@ func RunLogin(parentCtx context.Context, version string, platformClient api.Plat ctx, cancel := context.WithCancel(parentCtx) defer cancel() - app := NewApp(version, cancel) + app := NewApp(version, "", "", cancel) p := tea.NewProgram(app, tea.WithInput(os.Stdin), tea.WithOutput(os.Stdout)) runErrCh := make(chan error, 1) diff --git a/internal/ui/run_login_test.go b/internal/ui/run_login_test.go index 5455b8d..a69239c 100644 --- a/internal/ui/run_login_test.go +++ b/internal/ui/run_login_test.go @@ -96,7 +96,7 @@ func TestLoginFlow_DeviceFlowSuccess(t *testing.T) { mockStorage.EXPECT().GetAuthToken().Return("", errors.New("no token")) mockStorage.EXPECT().SetAuthToken(gomock.Any()).Return(nil) - tm := teatest.NewTestModel(t, NewApp("test", cancel), teatest.WithInitialTermSize(120, 40)) + tm := teatest.NewTestModel(t, NewApp("test", "", "", cancel), teatest.WithInitialTermSize(120, 40)) sender := testModelSender{tm: tm} platformClient := api.NewPlatformClient() @@ -153,7 +153,7 @@ func TestLoginFlow_DeviceFlowFailure_NotConfirmed(t *testing.T) { mockStorage := auth.NewMockAuthTokenStorage(ctrl) mockStorage.EXPECT().GetAuthToken().Return("", errors.New("no token")) - tm := teatest.NewTestModel(t, NewApp("test", cancel), teatest.WithInitialTermSize(120, 40)) + tm := teatest.NewTestModel(t, NewApp("test", "", "", cancel), teatest.WithInitialTermSize(120, 40)) sender := testModelSender{tm: tm} platformClient := api.NewPlatformClient() diff --git a/internal/ui/run_logout.go b/internal/ui/run_logout.go index 759593f..30f7d5e 100644 --- a/internal/ui/run_logout.go +++ b/internal/ui/run_logout.go @@ -15,7 +15,7 @@ func RunLogout(parentCtx context.Context) error { _, cancel := context.WithCancel(parentCtx) defer cancel() - app := NewApp("", cancel) + app := NewApp("", "", "", cancel) p := tea.NewProgram(app, tea.WithInput(os.Stdin), tea.WithOutput(os.Stdout)) runErrCh := make(chan error, 1) diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index 8f93e37..6f3972d 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -18,13 +18,6 @@ var ( NimboLight = lipgloss.NewStyle(). Foreground(lipgloss.Color(NimboLightColor)) - Title = lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("69")) - - Version = lipgloss.NewStyle(). - Foreground(lipgloss.Color("241")) - Message = lipgloss.NewStyle(). Foreground(lipgloss.Color("245"))