From 96f08cc0f663ae26aa2243e6ef9675593da62961 Mon Sep 17 00:00:00 2001 From: Ben Burkert Date: Tue, 14 May 2024 17:46:52 -0400 Subject: [PATCH] v0.0.35: improved error handling, nss bugfix, & WSL detection --- README.md | 13 ++-- api/api.go | 23 +++++-- auth/client.go | 2 +- auth/models/signin.go | 36 ++++------ auth/models/signout.go | 40 +++++------ auth/models/whoami.go | 15 ++--- auth/signin.go | 6 +- auth/signout.go | 4 +- auth/whoami.go | 2 +- cli.go | 27 +++++++- cli_test.go | 28 ++++++-- cmd.go | 12 ++-- lcl/audit.go | 4 +- lcl/clean.go | 2 +- lcl/config.go | 8 +-- lcl/lcl.go | 22 +++--- lcl/models/audit.go | 36 ++++------ lcl/models/clean.go | 15 ++--- lcl/models/config.go | 76 ++++++++------------- lcl/models/lcl.go | 85 +++++++++--------------- lcl/models/setup.go | 42 +++++------- lcl/setup.go | 4 +- testdata/TestError/golden-unix.golden | 30 ++++----- testdata/TestError/golden-windows.golden | 30 ++++----- testdata/TestPanic/golden-unix.golden | 30 ++++----- testdata/TestPanic/golden-windows.golden | 30 ++++----- trust/audit.go | 4 +- trust/clean.go | 2 +- trust/models/audit.go | 36 ++++------ trust/models/clean.go | 15 ++--- trust/models/trust.go | 56 +++++++++------- trust/runtime_detector.go | 48 +++++++++++++ trust/runtime_detector_test.go | 76 +++++++++++++++++++++ trust/testdata/TestTrust/wsl-vm.golden | 44 ++++++++++++ trust/trust.go | 8 ++- trust/trust_test.go | 43 ++++++++++++ truststore/nss.go | 28 +++----- truststore/nss_test.go | 45 +++++++++++++ ui/driver.go | 11 ++- ui/models.go | 42 ++++++++++++ ui/styles.go | 1 + 41 files changed, 671 insertions(+), 410 deletions(-) create mode 100644 trust/runtime_detector.go create mode 100644 trust/runtime_detector_test.go create mode 100644 trust/testdata/TestTrust/wsl-vm.golden create mode 100644 truststore/nss_test.go create mode 100644 ui/models.go diff --git a/README.md b/README.md index 2b04e55..cc85ab8 100644 --- a/README.md +++ b/README.md @@ -46,13 +46,18 @@ brew upgrade anchordotdev/tap/anchor ### Windows -Available via [Chocolatey][] or as a downloadable binary from the [releases page][]. +Available via [Winget][] or as a downloadable binary from the [releases page][]. -### Chocolatey +### Winget Install: ``` -chocolatey install anchor --version=0.0.22 +winget install anchor +``` + +Upgrade: +``` +winget upgrade anchor ``` ### Install from source @@ -62,7 +67,7 @@ Install: go install github.com/anchordotdev/cli/cmd/anchor@latest ``` -[Chocolatey]: https://community.chocolatey.org/ +[Winget]: https://learn.microsoft.com/en-us/windows/package-manager/winget/ [Homebrew]: https://brew.sh [releases page]: https://github.com/anchordotdev/cli/releases/latest diff --git a/api/api.go b/api/api.go index 3012fec..b3bdc5d 100644 --- a/api/api.go +++ b/api/api.go @@ -22,6 +22,7 @@ import ( var ( ErrSignedOut = errors.New("sign in required") + ErrTransient = errors.New("transient error encountered, please retry") ErrGnomeKeyringRequired = fmt.Errorf("gnome-keyring required for secure credential storage: %w", ErrSignedOut) ) @@ -123,6 +124,8 @@ func (s *Session) CreatePATToken(ctx context.Context, deviceCode string) (string return "", err } + requestId := res.Header.Get("X-Request-Id") + switch res.StatusCode { case http.StatusOK: var patTokens *AuthCliPatTokensResponse @@ -130,6 +133,8 @@ func (s *Session) CreatePATToken(ctx context.Context, deviceCode string) (string return "", err } return patTokens.PatToken, nil + case http.StatusServiceUnavailable: + return "", ErrTransient case http.StatusBadRequest: var errorsRes *Error if err = json.NewDecoder(res.Body).Decode(&errorsRes); err != nil { @@ -137,16 +142,16 @@ func (s *Session) CreatePATToken(ctx context.Context, deviceCode string) (string } switch errorsRes.Type { case "urn:anchordev:api:cli-auth:authorization-pending": - return "", nil + return "", ErrTransient case "urn:anchordev:api:cli-auth:expired-device-code": return "", fmt.Errorf("Your authorization request has expired, please try again.") case "urn:anchordev:api:cli-auth:incorrect-device-code": return "", fmt.Errorf("Your authorization request was not found, please try again.") default: - return "", fmt.Errorf("unexpected error: %s", errorsRes.Detail) + return "", fmt.Errorf("request [%s]: unexpected error: %s", requestId, errorsRes.Detail) } default: - return "", fmt.Errorf("unexpected response code: %d", res.StatusCode) + return "", fmt.Errorf("request [%s]: unexpected response code: %d", requestId, res.StatusCode) } } @@ -262,7 +267,8 @@ func (s *Session) get(ctx context.Context, path string, out any) error { if err = json.NewDecoder(res.Body).Decode(&errorsRes); err != nil { return err } - return fmt.Errorf("%w: %s", StatusCodeError(res.StatusCode), errorsRes.Title) + requestId := res.Header.Get("X-Request-Id") + return fmt.Errorf("request [%s]: %w: %s", requestId, StatusCodeError(res.StatusCode), errorsRes.Title) } return json.NewDecoder(res.Body).Decode(out) } @@ -292,7 +298,8 @@ func (s *Session) post(ctx context.Context, path string, in, out any) error { if err = json.NewDecoder(res.Body).Decode(&errorsRes); err != nil { return err } - return fmt.Errorf("%w: %s", StatusCodeError(res.StatusCode), errorsRes.Title) + requestId := res.Header.Get("X-Request-Id") + return fmt.Errorf("request [%s]: %w: %s", requestId, StatusCodeError(res.StatusCode), errorsRes.Title) } return json.NewDecoder(res.Body).Decode(out) } @@ -326,14 +333,16 @@ func (r responseChecker) RoundTrip(req *http.Request) (*http.Response, error) { return nil, fmt.Errorf("request error %s %s: %w", req.Method, req.URL.Path, err) } + requestId := res.Header.Get("X-Request-Id") + switch res.StatusCode { case http.StatusForbidden: return nil, ErrSignedOut case http.StatusInternalServerError: - return nil, fmt.Errorf("request failed: %w", err) + return nil, fmt.Errorf("request [%s] failed: %w", requestId, err) } if contentType := res.Header.Get("Content-Type"); !jsonMediaTypes.Matches(contentType) { - return nil, fmt.Errorf("non-json response received: %q: %w", contentType, err) + return nil, fmt.Errorf("request [%s]: %d response, expected json content-type, got: %q", requestId, res.StatusCode, contentType) } return res, nil } diff --git a/auth/client.go b/auth/client.go index 565081a..8f91d41 100644 --- a/auth/client.go +++ b/auth/client.go @@ -44,7 +44,7 @@ func (c Client) Perform(ctx context.Context, drv *ui.Driver) (*api.Session, erro if errors.Is(newClientErr, api.ErrSignedOut) || errors.Is(userInfoErr, api.ErrSignedOut) { if c.Hint == nil { - c.Hint = &models.SignInHint{} + c.Hint = models.SignInHint } cmd := &SignIn{ Hint: c.Hint, diff --git a/auth/models/signin.go b/auth/models/signin.go index bf222f9..f7e86fd 100644 --- a/auth/models/signin.go +++ b/auth/models/signin.go @@ -9,29 +9,21 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type SignInHeader struct{} - -func (SignInHeader) Init() tea.Cmd { return nil } - -func (m *SignInHeader) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *SignInHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Signin to Anchor.dev %s", ui.Whisper("`anchor auth signin`")))) - return b.String() -} - -type SignInHint struct{} - -func (SignInHint) Init() tea.Cmd { return nil } - -func (m SignInHint) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +var ( + SignInHeader = ui.Section{ + Name: "SignInHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Signin to Anchor.dev %s", ui.Whisper("`anchor auth signin`"))), + }, + } -func (m SignInHint) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.StepHint("Please sign up or sign in with your Anchor account.")) - return b.String() -} + SignInHint = ui.Section{ + Name: "SignInHint", + Model: ui.MessageLines{ + ui.StepHint("Please sign up or sign in with your Anchor account."), + }, + } +) type SignInPrompt struct { ConfirmCh chan<- struct{} diff --git a/auth/models/signout.go b/auth/models/signout.go index e4671a3..058d484 100644 --- a/auth/models/signout.go +++ b/auth/models/signout.go @@ -2,32 +2,22 @@ package models import ( "fmt" - "strings" "github.com/anchordotdev/cli/ui" - tea "github.com/charmbracelet/bubbletea" ) -type SignOutHeader struct{} - -func (SignOutHeader) Init() tea.Cmd { return nil } - -func (m *SignOutHeader) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *SignOutHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Signout from Anchor.dev %s", ui.Whisper("`anchor auth signout`")))) - return b.String() -} - -type SignOutSuccess struct{} - -func (SignOutSuccess) Init() tea.Cmd { return nil } - -func (m *SignOutSuccess) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *SignOutSuccess) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.StepDone("Signed out.")) - return b.String() -} +var ( + SignOutHeader = ui.Section{ + Name: "SignOutHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Signout from Anchor.dev %s", ui.Whisper("`anchor auth signout`"))), + }, + } + + SignOutSuccess = ui.Section{ + Name: "SignOutSuccess", + Model: ui.MessageLines{ + ui.StepDone("Signed out."), + }, + } +) diff --git a/auth/models/whoami.go b/auth/models/whoami.go index 50d7401..ec69781 100644 --- a/auth/models/whoami.go +++ b/auth/models/whoami.go @@ -9,16 +9,11 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type WhoAmIHeader struct{} - -func (m *WhoAmIHeader) Init() tea.Cmd { return nil } - -func (m *WhoAmIHeader) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *WhoAmIHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Identify Current Anchor.dev Account %s", ui.Whisper("`anchor auth whoami`")))) - return b.String() +var WhoAmIHeader = ui.Section{ + Name: "WhoAmIHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Identify Current Anchor.dev Account %s", ui.Whisper("`anchor auth whoami`"))), + }, } type WhoAmIChecker struct { diff --git a/auth/signin.go b/auth/signin.go index 9b19c4e..4bd586b 100644 --- a/auth/signin.go +++ b/auth/signin.go @@ -39,10 +39,10 @@ func (s SignIn) UI() cli.UI { func (s *SignIn) RunTUI(ctx context.Context, drv *ui.Driver) error { cfg := cli.ConfigFromContext(ctx) - drv.Activate(ctx, &models.SignInHeader{}) + drv.Activate(ctx, models.SignInHeader) if s.Hint == nil { - s.Hint = &models.SignInHint{} + s.Hint = models.SignInHint } drv.Activate(ctx, s.Hint) @@ -83,7 +83,7 @@ func (s *SignIn) RunTUI(ctx context.Context, drv *ui.Driver) error { var patToken string for patToken == "" { - if patToken, err = anc.CreatePATToken(ctx, codes.DeviceCode); err != nil { + if patToken, err = anc.CreatePATToken(ctx, codes.DeviceCode); err != nil && err != api.ErrTransient { return err } diff --git a/auth/signout.go b/auth/signout.go index 77f11d9..5b20fc8 100644 --- a/auth/signout.go +++ b/auth/signout.go @@ -23,13 +23,13 @@ func (s SignOut) UI() cli.UI { func (s *SignOut) runTUI(ctx context.Context, drv *ui.Driver) error { cfg := cli.ConfigFromContext(ctx) - drv.Activate(ctx, &models.SignOutHeader{}) + drv.Activate(ctx, models.SignOutHeader) kr := keyring.Keyring{Config: cfg} err := kr.Delete(keyring.APIToken) if err == nil { - drv.Activate(ctx, &models.SignOutSuccess{}) + drv.Activate(ctx, models.SignOutSuccess) } return err diff --git a/auth/whoami.go b/auth/whoami.go index d09ba8a..98b08af 100644 --- a/auth/whoami.go +++ b/auth/whoami.go @@ -27,7 +27,7 @@ func (c WhoAmI) UI() cli.UI { func (c *WhoAmI) runTUI(ctx context.Context, drv *ui.Driver) error { cfg := cli.ConfigFromContext(ctx) - drv.Activate(ctx, &models.WhoAmIHeader{}) + drv.Activate(ctx, models.WhoAmIHeader) drv.Activate(ctx, &models.WhoAmIChecker{}) anc, err := api.NewClient(cfg) diff --git a/cli.go b/cli.go index 9251222..d44f22d 100644 --- a/cli.go +++ b/cli.go @@ -4,11 +4,13 @@ import ( "context" "fmt" "go/build" + "io/fs" "net/url" "os" "regexp" "runtime" "strings" + "time" "github.com/anchordotdev/cli/models" "github.com/anchordotdev/cli/ui" @@ -17,6 +19,8 @@ import ( "github.com/spf13/pflag" ) +var Executable string + var Version = struct { Version, Commit, Date string @@ -120,6 +124,12 @@ type Config struct { } Version struct{} `cmd:"version"` + + // values used for testing + + GOOS string `desc:"change OS identifier in tests"` + ProcFS fs.FS `desc:"change the proc filesystem in tests"` + Timestamp time.Time `desc:"timestamp to use/display in tests"` } type UI struct { @@ -205,10 +215,25 @@ func ReportError(ctx context.Context, drv *ui.Driver, cmd *cobra.Command, args [ fmt.Fprintf(&body, "\n") fmt.Fprintf(&body, "---\n") fmt.Fprintf(&body, "\n") - fmt.Fprintf(&body, "**Command:** `%s`\n", cmd.CalledAs()) + fmt.Fprintf(&body, "**Command:** `%s`\n", cmd.CommandPath()) + var executable string + if Executable != "" { + executable = Executable + } else { + executable, _ = os.Executable() + } + if executable != "" { + fmt.Fprintf(&body, "**Executable:** `%s`\n", executable) + } fmt.Fprintf(&body, "**Version:** `%s`\n", VersionString()) fmt.Fprintf(&body, "**Arguments:** `[%s]`\n", strings.Join(args, ", ")) fmt.Fprintf(&body, "**Flags:** `[%s]`\n", strings.Join(flags, ", ")) + + timestamp := cfg.Timestamp + if timestamp.IsZero() { + timestamp = time.Now().UTC() + } + fmt.Fprintf(&body, "**Timestamp:** `%s`\n", timestamp.Format(time.RFC3339Nano)) if stack != "" { fmt.Fprintf(&body, "**Stack:**\n```\n%s\n```\n", normalizeStack(stack)) } diff --git a/cli_test.go b/cli_test.go index 219f4f4..f32feb1 100644 --- a/cli_test.go +++ b/cli_test.go @@ -14,9 +14,7 @@ import ( "github.com/anchordotdev/cli/ui" "github.com/anchordotdev/cli/ui/uitest" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/exp/teatest" - "github.com/muesli/termenv" "github.com/spf13/cobra" "github.com/stretchr/testify/require" ) @@ -24,16 +22,20 @@ import ( func setupCleanup(t *testing.T) { t.Helper() - colorProfile := lipgloss.ColorProfile() - lipgloss.SetColorProfile(termenv.TrueColor) - cliOS, cliArch := cli.Version.Os, cli.Version.Arch cli.Version.Os, cli.Version.Arch = "goos", "goarch" - t.Cleanup(func() { - lipgloss.SetColorProfile(colorProfile) + cliExecutable := cli.Executable + switch runtime.GOOS { + case "darwin", "linux": + cli.Executable = "/tmp/go-build0123456789/b001/exe/anchor" + case "windows": + cli.Executable = `C:\Users\username\AppData\Local\Temp\go-build0123456789/b001/exe/anchor.exe` + } + t.Cleanup(func() { cli.Version.Os, cli.Version.Arch = cliOS, cliArch + cli.Executable = cliExecutable }) } @@ -71,9 +73,14 @@ func TestError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + var err error cfg := cli.Config{} cfg.NonInteractive = true cfg.Test.Browserless = true + cfg.Timestamp = Timestamp + if err != nil { + t.Fatal(err) + } ctx = cli.ContextWithConfig(ctx, &cfg) t.Run(fmt.Sprintf("golden-%s", testTag()), func(t *testing.T) { @@ -117,9 +124,14 @@ func TestPanic(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + var err error cfg := cli.Config{} cfg.NonInteractive = true cfg.Test.Browserless = true + cfg.Timestamp = Timestamp + if err != nil { + t.Fatal(err) + } ctx = cli.ContextWithConfig(ctx, &cfg) t.Run(fmt.Sprintf("golden-%s", testTag()), func(t *testing.T) { @@ -168,3 +180,5 @@ func (m *TestHint) View() string { fmt.Fprintln(&b, ui.StepHint(fmt.Sprintf("Test %s Hint.", m.Type))) return b.String() } + +var Timestamp, _ = time.Parse(time.RFC3339Nano, "2024-01-02T15:04:05.987654321Z") diff --git a/cmd.go b/cmd.go index 03689c7..1e190e9 100644 --- a/cmd.go +++ b/cmd.go @@ -217,17 +217,17 @@ func NewCmd[T UIer](parent *cobra.Command, name string, fn func(*cobra.Command)) } cmd := &cobra.Command{ - Use: def.Use, - Args: def.Args, - Short: def.Short, - Long: def.Long, - SilenceUsage: true, + Use: def.Use, + Args: def.Args, + Short: def.Short, + Long: def.Long, + SilenceUsage: true, } ctx := ContextWithConfig(context.Background(), cfg) cmd.SetContext(ctx) - cmd.SetErrPrefix(ui.Error("")) + cmd.SetErrPrefix(ui.Error("Error!")) fn(cmd) diff --git a/lcl/audit.go b/lcl/audit.go index 362e997..e83fa33 100644 --- a/lcl/audit.go +++ b/lcl/audit.go @@ -37,8 +37,8 @@ func (c Audit) run(ctx context.Context, drv *ui.Driver) error { return err } - drv.Activate(ctx, &models.AuditHeader{}) - drv.Activate(ctx, &models.AuditHint{}) + drv.Activate(ctx, models.AuditHeader) + drv.Activate(ctx, models.AuditHint) _, err = c.perform(ctx, drv) if err != nil { diff --git a/lcl/clean.go b/lcl/clean.go index ec32106..a2272c1 100644 --- a/lcl/clean.go +++ b/lcl/clean.go @@ -51,7 +51,7 @@ func (c LclClean) run(ctx context.Context, drv *ui.Driver) error { c.realmSlug = "localhost" } - drv.Activate(ctx, &models.LclCleanHeader{}) + drv.Activate(ctx, models.LclCleanHeader) drv.Activate(ctx, &models.LclCleanHint{ TrustStores: cfg.Trust.Stores, }) diff --git a/lcl/config.go b/lcl/config.go index b4308dd..f94d749 100644 --- a/lcl/config.go +++ b/lcl/config.go @@ -52,8 +52,8 @@ func (c LclConfig) runTUI(ctx context.Context, drv *ui.Driver) error { return err } - drv.Activate(ctx, &models.LclConfigHeader{}) - drv.Activate(ctx, &models.LclConfigHint{}) + drv.Activate(ctx, models.LclConfigHeader) + drv.Activate(ctx, models.LclConfigHint) err = c.perform(ctx, drv) if err != nil { @@ -192,7 +192,7 @@ func (c LclConfig) perform(ctx context.Context, drv *ui.Driver) error { if !cfg.Trust.MockMode { if err := browser.OpenURL(httpURL.String()); err != nil { browserless = true - drv.Activate(ctx, &models.Browserless{}) + drv.Activate(ctx, models.Browserless) } } @@ -247,7 +247,7 @@ func (c LclConfig) perform(ctx context.Context, drv *ui.Driver) error { if !cfg.Trust.MockMode { if err := browser.OpenURL(httpsURL.String()); err != nil { browserless = true - drv.Activate(ctx, &models.Browserless{}) + drv.Activate(ctx, models.Browserless) } } diff --git a/lcl/lcl.go b/lcl/lcl.go index d84d7b1..fd90892 100644 --- a/lcl/lcl.go +++ b/lcl/lcl.go @@ -45,17 +45,17 @@ func (c *Command) run(ctx context.Context, drv *ui.Driver) error { var err error cmd := &auth.Client{ Anc: c.anc, - Hint: &models.LclSignInHint{}, + Hint: models.LclSignInHint, Source: "lclhost", } c.anc, err = cmd.Perform(ctx, drv) if err != nil { return err } - drv.Activate(ctx, &models.LclPreamble{}) + drv.Activate(ctx, models.LclPreamble) - drv.Activate(ctx, &models.LclHeader{}) - drv.Activate(ctx, &models.LclHint{}) + drv.Activate(ctx, models.LclHeader) + drv.Activate(ctx, models.LclHint) userInfo, err := c.anc.UserInfo(ctx) if err != nil { @@ -66,8 +66,8 @@ func (c *Command) run(ctx context.Context, drv *ui.Driver) error { realmSlug := "localhost" // run audit command - drv.Activate(ctx, &models.AuditHeader{}) - drv.Activate(ctx, &models.AuditHint{}) + drv.Activate(ctx, models.AuditHeader) + drv.Activate(ctx, models.AuditHint) cmdAudit := &Audit{ anc: c.anc, @@ -81,11 +81,11 @@ func (c *Command) run(ctx context.Context, drv *ui.Driver) error { } if lclAuditResult.diagnosticServiceExists && lclAuditResult.trusted { - drv.Activate(ctx, &models.LclConfigSkip{}) + drv.Activate(ctx, models.LclConfigSkip) } else { // run config command - drv.Activate(ctx, &models.LclConfigHeader{}) - drv.Activate(ctx, &models.LclConfigHint{}) + drv.Activate(ctx, models.LclConfigHeader) + drv.Activate(ctx, models.LclConfigHint) cmdConfig := &LclConfig{ anc: c.anc, @@ -99,8 +99,8 @@ func (c *Command) run(ctx context.Context, drv *ui.Driver) error { } // run setup command - drv.Activate(ctx, &models.SetupHeader{}) - drv.Activate(ctx, &models.SetupHint{}) + drv.Activate(ctx, models.SetupHeader) + drv.Activate(ctx, models.SetupHint) cmdSetup := &Setup{ anc: c.anc, diff --git a/lcl/models/audit.go b/lcl/models/audit.go index 7de6cf5..8e89db6 100644 --- a/lcl/models/audit.go +++ b/lcl/models/audit.go @@ -11,29 +11,21 @@ import ( type AuditUnauthenticated bool -type AuditHeader struct{} - -func (AuditHeader) Init() tea.Cmd { return nil } - -func (m *AuditHeader) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *AuditHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Audit lcl.host HTTPS Local Development Environment %s", ui.Whisper("`anchor lcl audit`")))) - return b.String() -} - -type AuditHint struct{} - -func (AuditHint) Init() tea.Cmd { return nil } - -func (m *AuditHint) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +var ( + AuditHeader = ui.Section{ + Name: "AuditHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Audit lcl.host HTTPS Local Development Environment %s", ui.Whisper("`anchor lcl audit`"))), + }, + } -func (m *AuditHint) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.StepHint("We'll begin by checking your system to determine what you need for your setup.")) - return b.String() -} + AuditHint = ui.Section{ + Name: "AuditHint", + Model: ui.MessageLines{ + ui.StepHint("We'll begin by checking your system to determine what you need for your setup."), + }, + } +) type AuditResourcesFoundMsg struct{} type AuditResourcesNotFoundMsg struct{} diff --git a/lcl/models/clean.go b/lcl/models/clean.go index 03aa4e9..6292f1b 100644 --- a/lcl/models/clean.go +++ b/lcl/models/clean.go @@ -9,16 +9,11 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type LclCleanHeader struct{} - -func (LclCleanHeader) Init() tea.Cmd { return nil } - -func (m *LclCleanHeader) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *LclCleanHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Clean lcl.host CA Certificates from Local Trust Store(s) %s", ui.Whisper("`anchor trust clean`")))) - return b.String() +var LclCleanHeader = ui.Section{ + Name: "LclCleanHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Clean lcl.host CA Certificates from Local Trust Store(s) %s", ui.Whisper("`anchor trust clean`"))), + }, } type LclCleanHint struct { diff --git a/lcl/models/config.go b/lcl/models/config.go index ab4e912..690aa62 100644 --- a/lcl/models/config.go +++ b/lcl/models/config.go @@ -8,44 +8,38 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type LclConfigSkip struct{} - -func (LclConfigSkip) Init() tea.Cmd { return nil } - -func (m *LclConfigSkip) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *LclConfigSkip) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Skip("Configure System for lcl.host Local Development `anchor lcl config`")) - return b.String() -} - -type LclConfigHeader struct{} - -func (LclConfigHeader) Init() tea.Cmd { return nil } - -func (m LclConfigHeader) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m LclConfigHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Configure System for lcl.host HTTPS Local Development %s", ui.Whisper("`anchor lcl config`")))) - return b.String() -} - -type LclConfigHint struct{} +var ( + LclConfigSkip = ui.Section{ + Name: "LclConfigSkip", + Model: ui.MessageLines{ + ui.Skip("Configure System for lcl.host Local Development `anchor lcl config`"), + }, + } -func (LclConfigHint) Init() tea.Cmd { return nil } + LclConfigHeader = ui.Section{ + Name: "LclConfigHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Configure System for lcl.host HTTPS Local Development %s", ui.Whisper("`anchor lcl config`"))), + }, + } -func (m LclConfigHint) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } + LclConfigHint = ui.Section{ + Name: "LclConfigHint", + Model: ui.MessageLines{ + ui.StepHint("Before issuing HTTPS certificates for your local applications, we need to"), + ui.StepHint("configure your browsers and OS to trust your personal certificates."), + ui.Whisper(" |"), // whisper instead of stephint to avoid whitespace errors from git + golden + ui.StepHint("We'll start a local diagnostic web server to guide you through the process."), + }, + } -func (m LclConfigHint) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.StepHint("Before issuing HTTPS certificates for your local applications, we need to")) - fmt.Fprintln(&b, ui.StepHint("configure your browsers and OS to trust your personal certificates.")) - fmt.Fprintln(&b, ui.Whisper(" |")) // whisper instead of stephint to avoid whitespace errors from git + golden - fmt.Fprintln(&b, ui.StepHint("We'll start a local diagnostic web server to guide you through the process.")) - return b.String() -} + Browserless = ui.Section{ + Name: "Browserless", + Model: ui.MessageLines{ + ui.Warning("Unable to open browser, skipping browser-based verification."), + }, + } +) type LclConfig struct { ConfirmCh chan<- struct{} @@ -139,15 +133,3 @@ func (m LclConfigSuccess) View() string { return b.String() } - -type Browserless struct{} - -func (m *Browserless) Init() tea.Cmd { return nil } - -func (m *Browserless) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *Browserless) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Warning("Unable to open browser, skipping browser-based verification.")) - return b.String() -} diff --git a/lcl/models/lcl.go b/lcl/models/lcl.go index 2c62142..8aae40a 100644 --- a/lcl/models/lcl.go +++ b/lcl/models/lcl.go @@ -12,61 +12,42 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type LclSignInHint struct{} - -func (LclSignInHint) Init() tea.Cmd { return nil } - -func (m *LclSignInHint) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *LclSignInHint) View() string { - var b strings.Builder - // FIXME: first line duplicated from SignInHint, should dedup somehow - fmt.Fprintln(&b, ui.StepHint("Please sign up or sign in with your Anchor account.")) - fmt.Fprintln(&b, ui.StepHint("")) - fmt.Fprintln(&b, ui.StepHint("Once authenticated, we can provision your personalized Anchor resources to")) - fmt.Fprintln(&b, ui.StepHint("power HTTPS in your local development environment.")) - return b.String() -} - -type LclPreamble struct{} - -func (LclPreamble) Init() tea.Cmd { return nil } - -func (m LclPreamble) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m LclPreamble) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Hint("Let's set up lcl.host HTTPS in your local development environment!")) - fmt.Fprintln(&b, ui.Hint("")) - fmt.Fprintln(&b, ui.Hint("lcl.host (made by the team at Anchor) adds HTTPS in a fast and totally free way")) - fmt.Fprintln(&b, ui.Hint("to local applications & services.")) - return b.String() -} - -type LclHeader struct{} - -func (LclHeader) Init() tea.Cmd { return nil } - -func (m LclHeader) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m LclHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Setup lcl.host HTTPS Local Development Environment %s", ui.Whisper("`anchor lcl`")))) - return b.String() -} - -type LclHint struct{} +var ( + LclSignInHint = ui.Section{ + Name: "LclSignInHint", + Model: ui.MessageLines{ + ui.StepHint("Please sign up or sign in with your Anchor account."), + ui.StepHint(""), + ui.StepHint("Once authenticated, we can provision your personalized Anchor resources to"), + ui.StepHint("power HTTPS in your local development environment."), + }, + } -func (LclHint) Init() tea.Cmd { return nil } + LclPreamble = ui.Section{ + Name: "LclPreamble", + Model: ui.MessageLines{ + ui.Hint("Let's set up lcl.host HTTPS in your local development environment!"), + ui.Hint(""), + ui.Hint("lcl.host (made by the team at Anchor) adds HTTPS in a fast and totally free way"), + ui.Hint("to local applications & services."), + }, + } -func (m LclHint) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } + LclHeader = ui.Section{ + Name: "LclHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Setup lcl.host HTTPS Local Development Environment %s", ui.Whisper("`anchor lcl`"))), + }, + } -func (m LclHint) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.StepHint("Once setup finishes, you'll have a secure context in your browsers and local")) - fmt.Fprintln(&b, ui.StepHint("system so you can use HTTPS locally.")) - return b.String() -} + LclHint = ui.Section{ + Name: "LclHint", + Model: ui.MessageLines{ + ui.StepHint("Once setup finishes, you'll have a secure context in your browsers and local"), + ui.StepHint("system so you can use HTTPS locally."), + }, + } +) type ProvisionService struct { Name, ServerType string diff --git a/lcl/models/setup.go b/lcl/models/setup.go index b74a266..8a58837 100644 --- a/lcl/models/setup.go +++ b/lcl/models/setup.go @@ -13,34 +13,22 @@ import ( "github.com/anchordotdev/cli/ui" ) -type SetupHeader struct{} - -func (m *SetupHeader) Init() tea.Cmd { return nil } - -func (m *SetupHeader) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *SetupHeader) View() string { - var b strings.Builder - - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Setup lcl.host Application %s", ui.Whisper("`anchor lcl setup`")))) - - return b.String() -} - -type SetupHint struct{} - -func (m *SetupHint) Init() tea.Cmd { return nil } - -func (m *SetupHint) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *SetupHint) View() string { - var b strings.Builder - - fmt.Fprintln(&b, ui.StepHint("We'll start by scanning your current directory, then ask you questions about")) - fmt.Fprintln(&b, ui.StepHint("your local application so that we can generate setup instructions for you.")) +var ( + SetupHeader = ui.Section{ + Name: "SetupHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Setup lcl.host Application %s", ui.Whisper("`anchor lcl setup`"))), + }, + } - return b.String() -} + SetupHint = ui.Section{ + Name: "SetupHint", + Model: ui.MessageLines{ + ui.StepHint("We'll start by scanning your current directory, then ask you questions about"), + ui.StepHint("your local application so that we can generate setup instructions for you."), + }, + } +) type SetupScan struct { finished bool diff --git a/lcl/setup.go b/lcl/setup.go index 059e5cb..4da8bae 100644 --- a/lcl/setup.go +++ b/lcl/setup.go @@ -53,8 +53,8 @@ func (c Setup) run(ctx context.Context, drv *ui.Driver) error { return err } - drv.Activate(ctx, &models.SetupHeader{}) - drv.Activate(ctx, &models.SetupHint{}) + drv.Activate(ctx, models.SetupHeader) + drv.Activate(ctx, models.SetupHint) err = c.perform(ctx, drv) if err != nil { diff --git a/testdata/TestError/golden-unix.golden b/testdata/TestError/golden-unix.golden index f539343..fd037d6 100644 --- a/testdata/TestError/golden-unix.golden +++ b/testdata/TestError/golden-unix.golden @@ -1,19 +1,19 @@ ─── TestHeader ───────────────────────────────────────────────────────────────── -# Test error `anchor test error` +# Test error `anchor test error` ─── TestHint ─────────────────────────────────────────────────────────────────── -# Test error `anchor test error` - | Test error Hint. +# Test error `anchor test error` + | Test error Hint. ─── ReportError ──────────────────────────────────────────────────────────────── -# Test error `anchor test error` - | Test error Hint. -# Error! test error `` - | We are sorry you encountered this error. - ! Press Enter to open an issue on Github. +# Test error `anchor test error` + | Test error Hint. +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. ─── Browserless ──────────────────────────────────────────────────────────────── -# Test error `anchor test error` - | Test error Hint. -# Error! test error `` - | We are sorry you encountered this error. - ! Press Enter to open an issue on Github. -! Warning: Unable to open browser. - ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%23+Test+error+%60anchor+test+error%60%0A++++%7C+Test+error+Hint.%0A%60%60%60%0A&title=Error%3A+test+error. +# Test error `anchor test error` + | Test error Hint. +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +! Warning: Unable to open browser. + ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60anchor%60%0A%2A%2AExecutable%3A%2A%2A+%60%2Ftmp%2Fgo-build0123456789%2Fb001%2Fexe%2Fanchor%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2ATimestamp%3A%2A%2A+%602024-01-02T15%3A04%3A05.987654321Z%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%23+Test+error+%60anchor+test+error%60%0A++++%7C+Test+error+Hint.%0A%60%60%60%0A&title=Error%3A+test+error. diff --git a/testdata/TestError/golden-windows.golden b/testdata/TestError/golden-windows.golden index f539343..072e37c 100644 --- a/testdata/TestError/golden-windows.golden +++ b/testdata/TestError/golden-windows.golden @@ -1,19 +1,19 @@ ─── TestHeader ───────────────────────────────────────────────────────────────── -# Test error `anchor test error` +# Test error `anchor test error` ─── TestHint ─────────────────────────────────────────────────────────────────── -# Test error `anchor test error` - | Test error Hint. +# Test error `anchor test error` + | Test error Hint. ─── ReportError ──────────────────────────────────────────────────────────────── -# Test error `anchor test error` - | Test error Hint. -# Error! test error `` - | We are sorry you encountered this error. - ! Press Enter to open an issue on Github. +# Test error `anchor test error` + | Test error Hint. +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. ─── Browserless ──────────────────────────────────────────────────────────────── -# Test error `anchor test error` - | Test error Hint. -# Error! test error `` - | We are sorry you encountered this error. - ! Press Enter to open an issue on Github. -! Warning: Unable to open browser. - ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%23+Test+error+%60anchor+test+error%60%0A++++%7C+Test+error+Hint.%0A%60%60%60%0A&title=Error%3A+test+error. +# Test error `anchor test error` + | Test error Hint. +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +! Warning: Unable to open browser. + ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60anchor%60%0A%2A%2AExecutable%3A%2A%2A+%60C%3A%5CUsers%5Cusername%5CAppData%5CLocal%5CTemp%5Cgo-build0123456789%2Fb001%2Fexe%2Fanchor.exe%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2ATimestamp%3A%2A%2A+%602024-01-02T15%3A04%3A05.987654321Z%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%23+Test+error+%60anchor+test+error%60%0A++++%7C+Test+error+Hint.%0A%60%60%60%0A&title=Error%3A+test+error. diff --git a/testdata/TestPanic/golden-unix.golden b/testdata/TestPanic/golden-unix.golden index 3a69dc1..494122e 100644 --- a/testdata/TestPanic/golden-unix.golden +++ b/testdata/TestPanic/golden-unix.golden @@ -1,19 +1,19 @@ ─── TestHeader ───────────────────────────────────────────────────────────────── -# Test panic `anchor test panic` +# Test panic `anchor test panic` ─── TestHint ─────────────────────────────────────────────────────────────────── -# Test panic `anchor test panic` - | Test panic Hint. +# Test panic `anchor test panic` + | Test panic Hint. ─── ReportError ──────────────────────────────────────────────────────────────── -# Test panic `anchor test panic` - | Test panic Hint. -# Error! test panic `` - | We are sorry you encountered this error. - ! Press Enter to open an issue on Github. +# Test panic `anchor test panic` + | Test panic Hint. +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. ─── Browserless ──────────────────────────────────────────────────────────────── -# Test panic `anchor test panic` - | Test panic Hint. -# Error! test panic `` - | We are sorry you encountered this error. - ! Press Enter to open an issue on Github. -! Warning: Unable to open browser. - ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09%3Cgoroot%3E%2Fsrc%2Fruntime%2Fpanic.go%3A%3Cline%3E+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28%3Chex%3E%2C+%7B%3Chex%3E%2C+%3Chex%3E%7D%2C+%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A111+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A140+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%23+Test+panic+%60anchor+test+panic%60%0A++++%7C+Test+panic+Hint.%0A%60%60%60%0A&title=Error%3A+test+panic. +# Test panic `anchor test panic` + | Test panic Hint. +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +! Warning: Unable to open browser. + ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60anchor%60%0A%2A%2AExecutable%3A%2A%2A+%60%2Ftmp%2Fgo-build0123456789%2Fb001%2Fexe%2Fanchor%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2ATimestamp%3A%2A%2A+%602024-01-02T15%3A04%3A05.987654321Z%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09%3Cgoroot%3E%2Fsrc%2Fruntime%2Fpanic.go%3A%3Cline%3E+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28%3Chex%3E%2C+%7B%3Chex%3E%2C+%3Chex%3E%7D%2C+%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A118+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A152+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%23+Test+panic+%60anchor+test+panic%60%0A++++%7C+Test+panic+Hint.%0A%60%60%60%0A&title=Error%3A+test+panic. diff --git a/testdata/TestPanic/golden-windows.golden b/testdata/TestPanic/golden-windows.golden index e2f317f..c2d0d76 100644 --- a/testdata/TestPanic/golden-windows.golden +++ b/testdata/TestPanic/golden-windows.golden @@ -1,19 +1,19 @@ ─── TestHeader ───────────────────────────────────────────────────────────────── -# Test panic `anchor test panic` +# Test panic `anchor test panic` ─── TestHint ─────────────────────────────────────────────────────────────────── -# Test panic `anchor test panic` - | Test panic Hint. +# Test panic `anchor test panic` + | Test panic Hint. ─── ReportError ──────────────────────────────────────────────────────────────── -# Test panic `anchor test panic` - | Test panic Hint. -# Error! test panic `` - | We are sorry you encountered this error. - ! Press Enter to open an issue on Github. +# Test panic `anchor test panic` + | Test panic Hint. +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. ─── Browserless ──────────────────────────────────────────────────────────────── -# Test panic `anchor test panic` - | Test panic Hint. -# Error! test panic `` - | We are sorry you encountered this error. - ! Press Enter to open an issue on Github. -! Warning: Unable to open browser. - ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09C%3A%2Fhostedtoolcache%2Fwindows%2Fgo%2F1.21.9%2Fx64%2Fsrc%2Fruntime%2Fpanic.go%3A920+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28%3Chex%3E%2C+%7B%3Chex%3E%2C+%3Chex%3E%7D%2C+%3Chex%3E%29%0A%09D%3A%2Fa%2Fanchor%2Fanchor%2Fcli%2Fcli_test.go%3A111+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09D%3A%2Fa%2Fanchor%2Fanchor%2Fcli%2Fcli_test.go%3A140+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09C%3A%2Fhostedtoolcache%2Fwindows%2Fgo%2F1.21.9%2Fx64%2Fsrc%2Ftesting%2Ftesting.go%3A1595+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09C%3A%2Fhostedtoolcache%2Fwindows%2Fgo%2F1.21.9%2Fx64%2Fsrc%2Ftesting%2Ftesting.go%3A1648+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%23+Test+panic+%60anchor+test+panic%60%0A++++%7C+Test+panic+Hint.%0A%60%60%60%0A&title=Error%3A+test+panic. +# Test panic `anchor test panic` + | Test panic Hint. +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +! Warning: Unable to open browser. + ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60anchor%60%0A%2A%2AExecutable%3A%2A%2A+%60C%3A%5CUsers%5Cusername%5CAppData%5CLocal%5CTemp%5Cgo-build0123456789%2Fb001%2Fexe%2Fanchor.exe%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2ATimestamp%3A%2A%2A+%602024-01-02T15%3A04%3A05.987654321Z%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09C%3A%2Fhostedtoolcache%2Fwindows%2Fgo%2F1.21.9%2Fx64%2Fsrc%2Fruntime%2Fpanic.go%3A920+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28%3Chex%3E%2C+%7B%3Chex%3E%2C+%3Chex%3E%7D%2C+%3Chex%3E%29%0A%09D%3A%2Fa%2Fanchor%2Fanchor%2Fcli%2Fcli_test.go%3A118+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09D%3A%2Fa%2Fanchor%2Fanchor%2Fcli%2Fcli_test.go%3A152+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09C%3A%2Fhostedtoolcache%2Fwindows%2Fgo%2F1.21.9%2Fx64%2Fsrc%2Ftesting%2Ftesting.go%3A1595+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09C%3A%2Fhostedtoolcache%2Fwindows%2Fgo%2F1.21.9%2Fx64%2Fsrc%2Ftesting%2Ftesting.go%3A1648+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%23+Test+panic+%60anchor+test+panic%60%0A++++%7C+Test+panic+Hint.%0A%60%60%60%0A&title=Error%3A+test+panic. diff --git a/trust/audit.go b/trust/audit.go index 1f91516..39a80a9 100644 --- a/trust/audit.go +++ b/trust/audit.go @@ -47,8 +47,8 @@ func (c *Audit) RunTUI(ctx context.Context, drv *ui.Driver) error { cfg := cli.ConfigFromContext(ctx) - drv.Activate(ctx, &models.TrustAuditHeader{}) - drv.Activate(ctx, &models.TrustAuditHint{}) + drv.Activate(ctx, models.TrustAuditHeader) + drv.Activate(ctx, models.TrustAuditHint) drv.Activate(ctx, &truststoremodels.TrustStoreAudit{}) diff --git a/trust/clean.go b/trust/clean.go index 8e878a4..a53290d 100644 --- a/trust/clean.go +++ b/trust/clean.go @@ -50,7 +50,7 @@ func (c *Clean) runTUI(ctx context.Context, drv *ui.Driver) error { return err } - drv.Activate(ctx, &models.TrustCleanHeader{}) + drv.Activate(ctx, models.TrustCleanHeader) drv.Activate(ctx, &models.TrustCleanHint{ CertStates: cfg.Trust.Clean.States, TrustStores: cfg.Trust.Stores, diff --git a/trust/models/audit.go b/trust/models/audit.go index a4bb707..bcb1e32 100644 --- a/trust/models/audit.go +++ b/trust/models/audit.go @@ -10,29 +10,21 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type TrustAuditHeader struct{} - -func (m *TrustAuditHeader) Init() tea.Cmd { return nil } - -func (m *TrustAuditHeader) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *TrustAuditHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Audit CA Certificates in Your Local Trust Store(s) %s", ui.Whisper("`anchor trust audit`")))) - return b.String() -} - -type TrustAuditHint struct{} - -func (m *TrustAuditHint) Init() tea.Cmd { return nil } - -func (m *TrustAuditHint) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +var ( + TrustAuditHeader = ui.Section{ + Name: "TrustAuditHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Audit CA Certificates in Your Local Trust Store(s) %s", ui.Whisper("`anchor trust audit`"))), + }, + } -func (m *TrustAuditHint) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.StepHint("We will compare your CA certificates from Anchor and your local trust stores.")) - return b.String() -} + TrustAuditHint = ui.Section{ + Name: "TrustAuditHint", + Model: ui.MessageLines{ + ui.StepHint("We will compare your CA certificates from Anchor and your local trust stores."), + }, + } +) type TrustAuditInfo struct { AuditInfo *truststore.AuditInfo diff --git a/trust/models/clean.go b/trust/models/clean.go index 7524d1c..1b8a631 100644 --- a/trust/models/clean.go +++ b/trust/models/clean.go @@ -13,16 +13,11 @@ import ( "github.com/anchordotdev/cli/ui" ) -type TrustCleanHeader struct{} - -func (m *TrustCleanHeader) Init() tea.Cmd { return nil } - -func (m *TrustCleanHeader) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *TrustCleanHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Clean CA Certificates from Local Trust Store(s) %s", ui.Whisper("`anchor trust clean`")))) - return b.String() +var TrustCleanHeader = ui.Section{ + Name: "TrustCleanHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Clean CA Certificates from Local Trust Store(s) %s", ui.Whisper("`anchor trust clean`"))), + }, } type TrustCleanHint struct { diff --git a/trust/models/trust.go b/trust/models/trust.go index 01872ad..da739ed 100644 --- a/trust/models/trust.go +++ b/trust/models/trust.go @@ -11,32 +11,24 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type TrustHeader struct{} - -func (m *TrustHeader) Init() tea.Cmd { return nil } - -func (m *TrustHeader) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - -func (m *TrustHeader) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.Header(fmt.Sprintf("Manage CA Certificates in your Local Trust Store(s) %s", ui.Whisper("`anchor trust`")))) - return b.String() -} - -type TrustHint struct{} - -func (m *TrustHint) Init() tea.Cmd { return nil } - -func (m *TrustHint) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +var ( + TrustHeader = ui.Section{ + Name: "TrustHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Manage CA Certificates in your Local Trust Store(s) %s", ui.Whisper("`anchor trust`"))), + }, + } -func (m *TrustHint) View() string { - var b strings.Builder - fmt.Fprintln(&b, ui.StepHint(fmt.Sprintf("%s %s", - ui.Accentuate("This may require sudo privileges, learn why here: "), - ui.URL("https://lcl.host/why-sudo"), - ))) - return b.String() -} + TrustHint = ui.Section{ + Name: "TrustHint", + Model: ui.MessageLines{ + ui.StepHint(fmt.Sprintf("%s %s", + ui.Accentuate("This may require sudo privileges, learn why here: "), + ui.URL("https://lcl.host/why-sudo"), + )), + }, + } +) type TrustUpdateConfirm struct { ConfirmCh chan<- struct{} @@ -150,3 +142,17 @@ func (m *TrustUpdateStore) View() string { return b.String() } + +type VMHint struct{} + +func (m *VMHint) Init() tea.Cmd { return nil } + +func (m *VMHint) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } + +func (m *VMHint) View() string { + var b strings.Builder + fmt.Fprintln(&b, ui.StepWarning("Running trust inside a VM or container will not update the host system.")) + fmt.Fprintln(&b, ui.StepHint("Rerun this command on your host system to update it's trust stores and enable")) // enable secure communication.")) + fmt.Fprintln(&b, ui.StepHint(fmt.Sprintf("secure communication, learn more here: %s", ui.URL("https://cl.host/vm-container-setup")))) + return b.String() +} diff --git a/trust/runtime_detector.go b/trust/runtime_detector.go new file mode 100644 index 0000000..408b16c --- /dev/null +++ b/trust/runtime_detector.go @@ -0,0 +1,48 @@ +package trust + +import ( + "io" + "os" + "runtime" + "strings" + + "github.com/anchordotdev/cli" +) + +func isVMOrContainer(cfg *cli.Config) bool { + os := cfg.GOOS + if os == "" { + os = runtime.GOOS + } + + switch os { + case "linux": + // only WSL is detected for now. + return isWSL(cfg) + default: + return false + } +} + +func isWSL(cfg *cli.Config) bool { + procFS := cfg.ProcFS + if procFS == nil { + procFS = os.DirFS("/proc") + } + + f, err := procFS.Open("version") + if err != nil { + return false + } + + buf, err := io.ReadAll(f) + if err != nil { + return false + } + kernel := string(buf) + + // https://superuser.com/questions/1725627/which-linux-kernel-do-i-have-in-wsl + + return strings.Contains(kernel, "Microsoft") || // WSL 1 + strings.Contains(kernel, "microsoft") // WSL 2 +} diff --git a/trust/runtime_detector_test.go b/trust/runtime_detector_test.go new file mode 100644 index 0000000..70ac28f --- /dev/null +++ b/trust/runtime_detector_test.go @@ -0,0 +1,76 @@ +package trust + +import ( + "testing" + "testing/fstest" + + "github.com/anchordotdev/cli" +) + +func TestIsVMOrContainer(t *testing.T) { + tests := []struct { + name string + + cfg *cli.Config + + result bool + }{ + { + name: "non-vm-or-container-linux", + + cfg: &cli.Config{ + GOOS: "linux", + ProcFS: fstest.MapFS{ + "version": &fstest.MapFile{ + Data: unameLinuxHost, + }, + }, + }, + + result: false, + }, + { + name: "WSL-1", + + cfg: &cli.Config{ + GOOS: "linux", + ProcFS: fstest.MapFS{ + "version": &fstest.MapFile{ + Data: unameWSL1, + }, + }, + }, + + result: true, + }, + { + name: "WSL-2", + + cfg: &cli.Config{ + GOOS: "linux", + ProcFS: fstest.MapFS{ + "version": &fstest.MapFile{ + Data: unameWSL2, + }, + }, + }, + + result: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if want, got := test.result, isVMOrContainer(test.cfg); want != got { + t.Errorf("want IsVMOrContainer result %t, got %t", want, got) + } + }) + } +} + +var ( + unameWSL1 = []byte(`Linux db-d-18 4.4.0-19041-Microsoft #1237-Microsoft Sat Sep 11 14:32:00 PST 2021 x86_64 GNU/Linux`) + unameWSL2 = []byte(`Linux db-d-18 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 GNU/Linux`) + + unameLinuxHost = []byte(`Linux geemus-framework 6.5.0-1020-oem #21-Ubuntu SMP PREEMPT_DYNAMIC Wed Apr 3 14:54:32 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux`) +) diff --git a/trust/testdata/TestTrust/wsl-vm.golden b/trust/testdata/TestTrust/wsl-vm.golden new file mode 100644 index 0000000..ea49fc9 --- /dev/null +++ b/trust/testdata/TestTrust/wsl-vm.golden @@ -0,0 +1,44 @@ +─── Client ───────────────────────────────────────────────────────────────────── + * Checking authentication: probing credentials locally…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + * Checking authentication: testing credentials remotely…⠋ +─── TrustHeader ──────────────────────────────────────────────────────────────── +# Manage CA Certificates in your Local Trust Store(s) `anchor trust` +─── TrustHint ────────────────────────────────────────────────────────────────── +# Manage CA Certificates in your Local Trust Store(s) `anchor trust` + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo +─── VMHint ───────────────────────────────────────────────────────────────────── +# Manage CA Certificates in your Local Trust Store(s) `anchor trust` + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + ! Warning: Running trust inside a VM or container will not update the host system. + | Rerun this command on your host system to update it's trust stores and enable + | secure communication, learn more here: https://cl.host/vm-container-setup +─── TrustStoreAudit ──────────────────────────────────────────────────────────── +# Manage CA Certificates in your Local Trust Store(s) `anchor trust` + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + ! Warning: Running trust inside a VM or container will not update the host system. + | Rerun this command on your host system to update it's trust stores and enable + | secure communication, learn more here: https://cl.host/vm-container-setup + * Comparing local and expected CA certificates…⠋ +─── TrustStoreAudit ──────────────────────────────────────────────────────────── +# Manage CA Certificates in your Local Trust Store(s) `anchor trust` + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + ! Warning: Running trust inside a VM or container will not update the host system. + | Rerun this command on your host system to update it's trust stores and enable + | secure communication, learn more here: https://cl.host/vm-container-setup + - Compared local and expected CA certificates: found matching certificates. +─── TrustUpdateConfirm ───────────────────────────────────────────────────────── +# Manage CA Certificates in your Local Trust Store(s) `anchor trust` + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + ! Warning: Running trust inside a VM or container will not update the host system. + | Rerun this command on your host system to update it's trust stores and enable + | secure communication, learn more here: https://cl.host/vm-container-setup + - Compared local and expected CA certificates: found matching certificates. + ! Press Enter to install missing certificates. (requires sudo) +─── TrustUpdateConfirm ───────────────────────────────────────────────────────── +# Manage CA Certificates in your Local Trust Store(s) `anchor trust` + | This may require sudo privileges, learn why here: https://lcl.host/why-sudo + ! Warning: Running trust inside a VM or container will not update the host system. + | Rerun this command on your host system to update it's trust stores and enable + | secure communication, learn more here: https://cl.host/vm-container-setup + - Compared local and expected CA certificates: found matching certificates. diff --git a/trust/trust.go b/trust/trust.go index 421bd0e..2d42cc8 100644 --- a/trust/trust.go +++ b/trust/trust.go @@ -54,8 +54,8 @@ func (c *Command) run(ctx context.Context, drv *ui.Driver) error { return err } - drv.Activate(ctx, &models.TrustHeader{}) - drv.Activate(ctx, &models.TrustHint{}) + drv.Activate(ctx, models.TrustHeader) + drv.Activate(ctx, models.TrustHint) err = c.Perform(ctx, drv) if err != nil { @@ -68,6 +68,10 @@ func (c *Command) run(ctx context.Context, drv *ui.Driver) error { func (c Command) Perform(ctx context.Context, drv *ui.Driver) error { cfg := cli.ConfigFromContext(ctx) + if isVMOrContainer(cfg) { + drv.Activate(ctx, &models.VMHint{}) + } + drv.Activate(ctx, &truststoremodels.TrustStoreAudit{}) var err error diff --git a/trust/trust_test.go b/trust/trust_test.go index 253254f..e10714a 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "testing" + "testing/fstest" "time" "github.com/anchordotdev/cli" @@ -124,4 +125,46 @@ func TestTrust(t *testing.T) { tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*3)) uitest.TestGolden(t, drv.Golden()) }) + + t.Run("wsl-vm", func(t *testing.T) { + if !srv.IsProxy() { + t.Skip("trust unsupported in mock mode") + } + + cfg := *cfg + + cfg.GOOS = "linux" + cfg.ProcFS = fstest.MapFS{ + "version": &fstest.MapFile{ + Data: unameWSL2, + }, + } + + ctx, cancel := context.WithCancel(cli.ContextWithConfig(ctx, &cfg)) + defer cancel() + + drv, tm := uitest.TestTUI(ctx, t) + + cmd := Command{} + + uitest.TestTUI(ctx, t) + + errc := make(chan error, 1) + go func() { + errc <- cmd.UI().RunTUI(ctx, drv) + + tm.Quit() + }() + + uitest.WaitForGoldenContains(t, drv, errc, + "! Press Enter to install missing certificates. (requires sudo)", + ) + + tm.Send(tea.KeyMsg{ + Type: tea.KeyEnter, + }) + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*3)) + uitest.TestGolden(t, drv.Golden()) + }) } diff --git a/truststore/nss.go b/truststore/nss.go index 63d01d4..5f03c82 100644 --- a/truststore/nss.go +++ b/truststore/nss.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "runtime" "strings" "sync" @@ -34,6 +35,8 @@ const ( certUtilSecReadOnlyOutput = "SEC_ERROR_READ_ONLY" ) +var trustAttributesRegexp = regexp.MustCompile(`\s+[a-zA-Z]?,[a-zA-z]?,[a-zA-Z]?\s*$`) + var firefoxPaths = []string{ "/usr/bin/firefox", "/usr/bin/firefox-nightly", @@ -269,32 +272,13 @@ func (s *NSS) ListCAs() ([]*CA, error) { } } - padLen := strings.Index(lines[0], "Trust Attributes") - if padLen <= 0 { - return NSSError{ - Err: errors.New("certutil unexpected output format"), - - CertutilInstallHelp: s.certutilInstallHelp, - NSSBrowsers: nssBrowsers, - } - } - if len(lines) == 2 { return nil // no certs in the output } var nicks []string for _, line := range lines[3:] { - if len(line) < padLen { - return NSSError{ - Err: errors.New("certutil unexpected line format"), - - CertutilInstallHelp: s.certutilInstallHelp, - NSSBrowsers: nssBrowsers, - } - } - - nicks = append(nicks, strings.TrimSpace(line[:padLen])) + nicks = append(nicks, parseCertNick(line)) } for _, nick := range nicks { @@ -455,3 +439,7 @@ func (s *NSS) handleCertUtilResult(profile string, out []byte, err error) error return nil } + +func parseCertNick(line string) string { + return trustAttributesRegexp.ReplaceAllString(line, "") +} diff --git a/truststore/nss_test.go b/truststore/nss_test.go new file mode 100644 index 0000000..5fd7f47 --- /dev/null +++ b/truststore/nss_test.go @@ -0,0 +1,45 @@ +package truststore + +import ( + "testing" +) + +func TestParseCertNick(t *testing.T) { + var tests = []struct { + name string + + line string + + nick string + }{ + { + name: "normal", + + line: "f016d6d279570cd2ac25debd C,, ", + + nick: "f016d6d279570cd2ac25debd", + }, + { + name: "long", + + line: "docert development CA FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF C,, ", + + nick: "docert development CA FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + }, + { + name: "very-long", + + line: "Baddy Local Authority - 2024 ECC Root 000000000000000000000000000000000000000 C,, ", + + nick: "Baddy Local Authority - 2024 ECC Root 000000000000000000000000000000000000000", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if want, got := test.nick, parseCertNick(test.line); want != got { + t.Errorf("want parsed cert nick %q, got %q", want, got) + } + }) + } +} diff --git a/ui/driver.go b/ui/driver.go index a216dff..cc24f18 100644 --- a/ui/driver.go +++ b/ui/driver.go @@ -181,7 +181,16 @@ func (d *Driver) View() string { } normalizedOut := spinnerReplacer.Replace(out) if out != "" && normalizedOut != d.lastView { - separator := fmt.Sprintf("─── %s ", reflect.TypeOf(d.active).Elem().Name()) + var section string + if mdl, ok := d.active.(interface{ Section() string }); ok { + section = mdl.Section() + } else if kind := reflect.TypeOf(d.active).Kind(); kind == reflect.Interface || kind == reflect.Pointer { + section = reflect.TypeOf(d.active).Elem().Name() + } else { + section = reflect.TypeOf(d.active).Name() + } + + separator := fmt.Sprintf("─── %s ", section) separator = separator + strings.Repeat("─", 80-utf8.RuneCountInString(separator)) d.lastView = normalizedOut diff --git a/ui/models.go b/ui/models.go new file mode 100644 index 0000000..e6507f4 --- /dev/null +++ b/ui/models.go @@ -0,0 +1,42 @@ +package ui + +import ( + "fmt" + "strings" + + tea "github.com/charmbracelet/bubbletea" +) + +type MessageLines []string + +func (MessageLines) Init() tea.Cmd { return nil } + +func (m MessageLines) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } + +func (m MessageLines) View() string { + var b strings.Builder + for _, line := range m { + fmt.Fprintln(&b, line) + } + return b.String() +} + +type MessageFunc func(*strings.Builder) + +func (MessageFunc) Init() tea.Cmd { return nil } + +func (m MessageFunc) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } + +func (m MessageFunc) View() string { + var b strings.Builder + m(&b) + return b.String() +} + +type Section struct { + Name string + + tea.Model +} + +func (s Section) Section() string { return s.Name } diff --git a/ui/styles.go b/ui/styles.go index 9f07a58..cf00ed5 100644 --- a/ui/styles.go +++ b/ui/styles.go @@ -28,6 +28,7 @@ var ( StepHint = hint.Copy().SetString(" |").Render StepInProgress = lipgloss.NewStyle().SetString(" *").Render StepPrompt = lipgloss.NewStyle().SetString(" " + Prompt.Render("?")).Render + StepWarning = header.Copy().SetString(" " + bgBanana(fgMidnight("!")) + fgBanana(" Warning:")).Render Accentuate = lipgloss.NewStyle().Italic(true).Render Action = lipgloss.NewStyle().Bold(true).Foreground(colorBrandPrimary).Render