From 433fec6233a57e3f4d16f4e6807573b67672e482 Mon Sep 17 00:00:00 2001 From: gerep <181501+gerep@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:30:42 -0300 Subject: [PATCH] add tip --- README.md | 31 ++++++++++++++++++++++++++++++- cmd/configure.go | 1 + cmd/localtest.go | 2 +- cmd/submit.go | 2 +- render/models.go | 21 +++++++++++---------- render/render.go | 8 ++++++-- render/view.go | 7 +++++++ render/view_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 102 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6459e8c..d0b27ff 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This is the official command line tool for [Boot.dev](https://www.boot.dev/). It - [1. Install Go](#1-install-go) - [2. Install the Boot.dev CLI](#2-install-the-bootdev-cli) - [3. Login to the CLI](#3-login-to-the-cli) +- [Running and Submitting Lessons](#running-and-submitting-lessons) - [Configuration](#configuration) - [Base URL for HTTP tests](#base-url-for-http-tests) - [CLI colors](#cli-colors) @@ -114,6 +115,34 @@ fish_add_path $HOME/go/bin Run `bootdev login` to authenticate with your Boot.dev account. After authenticating, you're ready to go! +## Running and Submitting Lessons + +When you reach a CLI lesson, the Boot.dev CLI can detect it from your course progress. Run the lesson's checks without submitting it with: + +```sh +bootdev run +``` + +Submit the lesson for completion with: + +```sh +bootdev submit +``` + +The lesson UUID is optional. You can still provide it explicitly to either command: + +```sh +bootdev run UUID +bootdev submit UUID +``` + +To run the checks and submit the lesson in one command, use the `--submit` (`-s`) flag. The UUID is optional here as well: + +```sh +bootdev run -s +bootdev run -s UUID +``` + ## Configuration The Boot.dev CLI offers a couple of configuration options that are stored in a config file (default is `~/.bootdev.yaml`, or `$XDG_CONFIG_HOME/bootdev/config.yaml` if `XDG_CONFIG_HOME` is set). @@ -151,7 +180,7 @@ The CLI text output is rendered with extra colors: green (e.g., success messages - To customize these colors, run: ```sh - bootdev config colors --red VALUE --green VALUE --gray VALUE + bootdev config colors --red VALUE --green VALUE --gray VALUE --magenta VALUE --yellow VALUE ``` _You can use an [ANSI color code](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) or a hex string as the `VALUE`._ diff --git a/cmd/configure.go b/cmd/configure.go index 8499a99..4228d40 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -22,6 +22,7 @@ var defaultColors = map[string]string{ "red": "1", "green": "2", "magenta": "5", + "yellow": "#f6bd45", } // configureColorsCmd represents the `configure colors` command for changing diff --git a/cmd/localtest.go b/cmd/localtest.go index ec45bd4..dff8f97 100644 --- a/cmd/localtest.go +++ b/cmd/localtest.go @@ -45,7 +45,7 @@ func localTestHandler(cmd *cobra.Command, args []string) error { fmt.Printf("You can reset to the default with `bootdev config base_url --reset`\n\n") } - send, finish := render.StartRenderer(true, verboseOutput) + send, finish := render.StartRenderer(true, verboseOutput, false) submissionEvent := api.LessonSubmissionEvent{} defer func() { finish(submissionEvent) diff --git a/cmd/submit.go b/cmd/submit.go index e0c7f78..0efde79 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -101,7 +101,7 @@ func submissionHandler(cmd *cobra.Command, args []string) error { fmt.Printf("You can reset to the default with `bootdev config base_url --reset`\n\n") } - send, finish := render.StartRenderer(isSubmit, verboseOutput) + send, finish := render.StartRenderer(isSubmit, verboseOutput, isSubmit && len(args) > 0) finalEvent := api.LessonSubmissionEvent{} var debugPath string var debugWriteErr error diff --git a/render/models.go b/render/models.go index 47160b6..dc135de 100644 --- a/render/models.go +++ b/render/models.go @@ -23,16 +23,17 @@ type stepModel struct { } type rootModel struct { - steps []stepModel - spinner spinner.Model - result api.VerificationResultSlug - failure *api.StructuredErrCLI - xpReward int - xpBreakdown []api.XPBreakdownItem - isSubmit bool - verbose bool - finalized bool - clear bool + steps []stepModel + spinner spinner.Model + result api.VerificationResultSlug + failure *api.StructuredErrCLI + xpReward int + xpBreakdown []api.XPBreakdownItem + isSubmit bool + verbose bool + showOmitLessonIDTip bool + finalized bool + clear bool } func initModel(isSubmit bool, verbose bool) rootModel { diff --git a/render/render.go b/render/render.go index d1d0274..29a2aab 100644 --- a/render/render.go +++ b/render/render.go @@ -16,6 +16,7 @@ var ( green lipgloss.Style red lipgloss.Style magenta lipgloss.Style + yellow lipgloss.Style gray lipgloss.Style white lipgloss.Style borderBox = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()) @@ -25,6 +26,7 @@ func (m rootModel) Init() tea.Cmd { green = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.green"))) red = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.red"))) magenta = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.magenta"))) + yellow = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.yellow"))) gray = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.gray"))) white = lipgloss.NewStyle().Foreground(lipgloss.Color("15")) return m.spinner.Tick @@ -102,8 +104,10 @@ func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } -func StartRenderer(isSubmit bool, verbose bool) (func(tea.Msg), func(api.LessonSubmissionEvent)) { - p := tea.NewProgram(initModel(isSubmit, verbose), tea.WithoutSignalHandler()) +func StartRenderer(isSubmit bool, verbose bool, showOmitLessonIDTip bool) (func(tea.Msg), func(api.LessonSubmissionEvent)) { + m := initModel(isSubmit, verbose) + m.showOmitLessonIDTip = showOmitLessonIDTip + p := tea.NewProgram(m, tea.WithoutSignalHandler()) done := make(chan struct{}) go func() { diff --git a/render/view.go b/render/view.go index d6964e4..558a33b 100644 --- a/render/view.go +++ b/render/view.go @@ -168,6 +168,13 @@ func (m rootModel) View() string { str.WriteString(green.Render("Return to your browser to continue with the next lesson.")) str.WriteByte('\n') str.WriteByte('\n') + if m.showOmitLessonIDTip { + str.WriteString(yellow.Render("Tip:")) + str.WriteString(" When you reach your next CLI lesson, you can skip copying its ID:\n ") + str.WriteString(yellow.Render("bootdev run")) + str.WriteByte('\n') + str.WriteByte('\n') + } } else if m.result == api.VerificationResultSlugSystemError { str.WriteByte('\n') str.WriteByte('\n') diff --git a/render/view_test.go b/render/view_test.go index 6703506..1ff6718 100644 --- a/render/view_test.go +++ b/render/view_test.go @@ -171,6 +171,51 @@ func TestSystemErrorViewDoesNotShowStepsAsPassed(t *testing.T) { } } +func TestOmitLessonIDTipOnlyAppearsAfterExplicitSuccessfulSubmission(t *testing.T) { + tests := []struct { + name string + isSubmit bool + showTip bool + result api.VerificationResultSlug + wantTip bool + }{ + {name: "explicit successful submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugSuccess, wantTip: true}, + {name: "UUID-less successful submission", isSubmit: true, result: api.VerificationResultSlugSuccess}, + {name: "ordinary run with UUID", showTip: true, result: api.VerificationResultSlugSuccess}, + {name: "failed submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugFailure}, + {name: "noop submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugNoop}, + {name: "canceled or pre-submission error", isSubmit: true, showTip: true}, + {name: "system-error submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugSystemError}, + } + + const tip = "Tip: When you reach your next CLI lesson, you can skip copying its ID:\n bootdev run" + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := initModel(tt.isSubmit, false) + m.finalized = true + m.showOmitLessonIDTip = tt.showTip + m.result = tt.result + if tt.result == api.VerificationResultSlugFailure || tt.result == api.VerificationResultSlugNoop { + m.failure = &api.StructuredErrCLI{FailedStepIndex: 0, ErrorMessage: "failed"} + } + + view := m.View() + if got := strings.Contains(view, tip); got != tt.wantTip { + t.Fatalf("tip present = %v, want %v\n%s", got, tt.wantTip, view) + } + if tt.wantTip { + if strings.Contains(view, "detect") { + t.Errorf("tip should not explain automatic detection\n%s", view) + } + browserInstruction := "Return to your browser to continue with the next lesson." + if !strings.Contains(view, browserInstruction) || strings.Index(view, browserInstruction) > strings.Index(view, tip) { + t.Fatalf("browser instruction must appear before tip\n%s", view) + } + } + }) + } +} + func TestStartStepFallsBackToTechnicalDescription(t *testing.T) { m := initModel(true, false) updated, _ := m.Update(messages.StartStepMsg{CMD: "go test ./..."})