Skip to content

Commit 433fec6

Browse files
committed
add tip
1 parent 3974116 commit 433fec6

8 files changed

Lines changed: 102 additions & 15 deletions

File tree

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This is the official command line tool for [Boot.dev](https://www.boot.dev/). It
1818
- [1. Install Go](#1-install-go)
1919
- [2. Install the Boot.dev CLI](#2-install-the-bootdev-cli)
2020
- [3. Login to the CLI](#3-login-to-the-cli)
21+
- [Running and Submitting Lessons](#running-and-submitting-lessons)
2122
- [Configuration](#configuration)
2223
- [Base URL for HTTP tests](#base-url-for-http-tests)
2324
- [CLI colors](#cli-colors)
@@ -114,6 +115,34 @@ fish_add_path $HOME/go/bin
114115

115116
Run `bootdev login` to authenticate with your Boot.dev account. After authenticating, you're ready to go!
116117

118+
## Running and Submitting Lessons
119+
120+
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:
121+
122+
```sh
123+
bootdev run
124+
```
125+
126+
Submit the lesson for completion with:
127+
128+
```sh
129+
bootdev submit
130+
```
131+
132+
The lesson UUID is optional. You can still provide it explicitly to either command:
133+
134+
```sh
135+
bootdev run UUID
136+
bootdev submit UUID
137+
```
138+
139+
To run the checks and submit the lesson in one command, use the `--submit` (`-s`) flag. The UUID is optional here as well:
140+
141+
```sh
142+
bootdev run -s
143+
bootdev run -s UUID
144+
```
145+
117146
## Configuration
118147

119148
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
151180
- To customize these colors, run:
152181

153182
```sh
154-
bootdev config colors --red VALUE --green VALUE --gray VALUE
183+
bootdev config colors --red VALUE --green VALUE --gray VALUE --magenta VALUE --yellow VALUE
155184
```
156185

157186
_You can use an [ANSI color code](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) or a hex string as the `VALUE`._

cmd/configure.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var defaultColors = map[string]string{
2222
"red": "1",
2323
"green": "2",
2424
"magenta": "5",
25+
"yellow": "#f6bd45",
2526
}
2627

2728
// configureColorsCmd represents the `configure colors` command for changing

cmd/localtest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func localTestHandler(cmd *cobra.Command, args []string) error {
4545
fmt.Printf("You can reset to the default with `bootdev config base_url --reset`\n\n")
4646
}
4747

48-
send, finish := render.StartRenderer(true, verboseOutput)
48+
send, finish := render.StartRenderer(true, verboseOutput, false)
4949
submissionEvent := api.LessonSubmissionEvent{}
5050
defer func() {
5151
finish(submissionEvent)

cmd/submit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func submissionHandler(cmd *cobra.Command, args []string) error {
101101
fmt.Printf("You can reset to the default with `bootdev config base_url --reset`\n\n")
102102
}
103103

104-
send, finish := render.StartRenderer(isSubmit, verboseOutput)
104+
send, finish := render.StartRenderer(isSubmit, verboseOutput, isSubmit && len(args) > 0)
105105
finalEvent := api.LessonSubmissionEvent{}
106106
var debugPath string
107107
var debugWriteErr error

render/models.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,17 @@ type stepModel struct {
2323
}
2424

2525
type rootModel struct {
26-
steps []stepModel
27-
spinner spinner.Model
28-
result api.VerificationResultSlug
29-
failure *api.StructuredErrCLI
30-
xpReward int
31-
xpBreakdown []api.XPBreakdownItem
32-
isSubmit bool
33-
verbose bool
34-
finalized bool
35-
clear bool
26+
steps []stepModel
27+
spinner spinner.Model
28+
result api.VerificationResultSlug
29+
failure *api.StructuredErrCLI
30+
xpReward int
31+
xpBreakdown []api.XPBreakdownItem
32+
isSubmit bool
33+
verbose bool
34+
showOmitLessonIDTip bool
35+
finalized bool
36+
clear bool
3637
}
3738

3839
func initModel(isSubmit bool, verbose bool) rootModel {

render/render.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var (
1616
green lipgloss.Style
1717
red lipgloss.Style
1818
magenta lipgloss.Style
19+
yellow lipgloss.Style
1920
gray lipgloss.Style
2021
white lipgloss.Style
2122
borderBox = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
@@ -25,6 +26,7 @@ func (m rootModel) Init() tea.Cmd {
2526
green = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.green")))
2627
red = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.red")))
2728
magenta = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.magenta")))
29+
yellow = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.yellow")))
2830
gray = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.gray")))
2931
white = lipgloss.NewStyle().Foreground(lipgloss.Color("15"))
3032
return m.spinner.Tick
@@ -102,8 +104,10 @@ func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
102104
}
103105
}
104106

105-
func StartRenderer(isSubmit bool, verbose bool) (func(tea.Msg), func(api.LessonSubmissionEvent)) {
106-
p := tea.NewProgram(initModel(isSubmit, verbose), tea.WithoutSignalHandler())
107+
func StartRenderer(isSubmit bool, verbose bool, showOmitLessonIDTip bool) (func(tea.Msg), func(api.LessonSubmissionEvent)) {
108+
m := initModel(isSubmit, verbose)
109+
m.showOmitLessonIDTip = showOmitLessonIDTip
110+
p := tea.NewProgram(m, tea.WithoutSignalHandler())
107111
done := make(chan struct{})
108112

109113
go func() {

render/view.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ func (m rootModel) View() string {
168168
str.WriteString(green.Render("Return to your browser to continue with the next lesson."))
169169
str.WriteByte('\n')
170170
str.WriteByte('\n')
171+
if m.showOmitLessonIDTip {
172+
str.WriteString(yellow.Render("Tip:"))
173+
str.WriteString(" When you reach your next CLI lesson, you can skip copying its ID:\n ")
174+
str.WriteString(yellow.Render("bootdev run"))
175+
str.WriteByte('\n')
176+
str.WriteByte('\n')
177+
}
171178
} else if m.result == api.VerificationResultSlugSystemError {
172179
str.WriteByte('\n')
173180
str.WriteByte('\n')

render/view_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,51 @@ func TestSystemErrorViewDoesNotShowStepsAsPassed(t *testing.T) {
171171
}
172172
}
173173

174+
func TestOmitLessonIDTipOnlyAppearsAfterExplicitSuccessfulSubmission(t *testing.T) {
175+
tests := []struct {
176+
name string
177+
isSubmit bool
178+
showTip bool
179+
result api.VerificationResultSlug
180+
wantTip bool
181+
}{
182+
{name: "explicit successful submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugSuccess, wantTip: true},
183+
{name: "UUID-less successful submission", isSubmit: true, result: api.VerificationResultSlugSuccess},
184+
{name: "ordinary run with UUID", showTip: true, result: api.VerificationResultSlugSuccess},
185+
{name: "failed submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugFailure},
186+
{name: "noop submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugNoop},
187+
{name: "canceled or pre-submission error", isSubmit: true, showTip: true},
188+
{name: "system-error submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugSystemError},
189+
}
190+
191+
const tip = "Tip: When you reach your next CLI lesson, you can skip copying its ID:\n bootdev run"
192+
for _, tt := range tests {
193+
t.Run(tt.name, func(t *testing.T) {
194+
m := initModel(tt.isSubmit, false)
195+
m.finalized = true
196+
m.showOmitLessonIDTip = tt.showTip
197+
m.result = tt.result
198+
if tt.result == api.VerificationResultSlugFailure || tt.result == api.VerificationResultSlugNoop {
199+
m.failure = &api.StructuredErrCLI{FailedStepIndex: 0, ErrorMessage: "failed"}
200+
}
201+
202+
view := m.View()
203+
if got := strings.Contains(view, tip); got != tt.wantTip {
204+
t.Fatalf("tip present = %v, want %v\n%s", got, tt.wantTip, view)
205+
}
206+
if tt.wantTip {
207+
if strings.Contains(view, "detect") {
208+
t.Errorf("tip should not explain automatic detection\n%s", view)
209+
}
210+
browserInstruction := "Return to your browser to continue with the next lesson."
211+
if !strings.Contains(view, browserInstruction) || strings.Index(view, browserInstruction) > strings.Index(view, tip) {
212+
t.Fatalf("browser instruction must appear before tip\n%s", view)
213+
}
214+
}
215+
})
216+
}
217+
}
218+
174219
func TestStartStepFallsBackToTechnicalDescription(t *testing.T) {
175220
m := initModel(true, false)
176221
updated, _ := m.Update(messages.StartStepMsg{CMD: "go test ./..."})

0 commit comments

Comments
 (0)