├── .gitignore
├── go.mod
├── README.md
├── input.go
├── main.go
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | debug.log
2 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module wizard-tutorial
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/charmbracelet/bubbles v0.14.0
7 | github.com/charmbracelet/bubbletea v0.23.1
8 | github.com/charmbracelet/lipgloss v0.6.0
9 | )
10 |
11 | require (
12 | github.com/atotto/clipboard v0.1.4 // indirect
13 | github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
14 | github.com/containerd/console v1.0.3 // indirect
15 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
16 | github.com/mattn/go-isatty v0.0.16 // indirect
17 | github.com/mattn/go-localereader v0.0.1 // indirect
18 | github.com/mattn/go-runewidth v0.0.14 // indirect
19 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
20 | github.com/muesli/cancelreader v0.2.2 // indirect
21 | github.com/muesli/reflow v0.3.0 // indirect
22 | github.com/muesli/termenv v0.13.0 // indirect
23 | github.com/rivo/uniseg v0.2.0 // indirect
24 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
25 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
26 | golang.org/x/text v0.3.7 // indirect
27 | )
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Companion: Build a Terminal Wizard in Go
2 |
3 |
4 |
5 | This is a companion project for [Build a Terminal Wizard in Go][vid], a video
6 | tutorial for getting started with [Bubble Tea][bubbletea] and
7 | [Lip Gloss][lipgloss].
8 |
9 | The commits for this project correspond to the milestones in the video to help
10 | you follow along.
11 |
12 | [vid]: https://youtu.be/Gl31diSVP8M
13 | [bubbletea]: https://github.com/charmbracelet/bubbletea
14 | [lipgloss]: https://github.com/charmbracelet/lipgloss
15 |
16 | ## Feedback
17 |
18 | Whatcha think? We’d love to hear your thoughts on this project: Feel free to
19 | drop a note in [the comments section of the video][vid] or on:
20 |
21 | * [Twitter](https://twitter.com/charmcli)
22 | * [The Fediverse](https://mastodon.social/@charmcli)
23 | * [Discord](https://charm.sh/chat)
24 |
25 | ## License
26 |
27 | [MIT](https://github.com/charmbracelet/vhs/raw/main/LICENSE)
28 |
29 | ***
30 |
31 | Part of [Charm](https://charm.sh).
32 |
33 |
34 |
39 |
40 |
41 | Charm热爱开源 • Charm loves open source
42 |
--------------------------------------------------------------------------------
/input.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/charmbracelet/bubbles/textarea"
5 | "github.com/charmbracelet/bubbles/textinput"
6 | tea "github.com/charmbracelet/bubbletea"
7 | )
8 |
9 | type Input interface {
10 | Blink() tea.Msg
11 | Blur() tea.Msg
12 | Focus() tea.Cmd
13 | SetValue(string)
14 | Value() string
15 | Update(tea.Msg) (Input, tea.Cmd)
16 | View() string
17 | }
18 |
19 | // We need to have a wrapper for our bubbles as they don't currently implement the tea.Model interface
20 | // textinput, textarea
21 |
22 | type shortAnswerField struct {
23 | textinput textinput.Model
24 | }
25 |
26 | func NewShortAnswerField() *shortAnswerField {
27 | a := shortAnswerField{}
28 |
29 | model := textinput.New()
30 | model.Placeholder = "Your answer here"
31 | model.Focus()
32 |
33 | a.textinput = model
34 | return &a
35 | }
36 |
37 | func (a *shortAnswerField) Blink() tea.Msg {
38 | return textinput.Blink()
39 | }
40 |
41 | func (a *shortAnswerField) Init() tea.Cmd {
42 | return nil
43 | }
44 |
45 | func (a *shortAnswerField) Update(msg tea.Msg) (Input, tea.Cmd) {
46 | var cmd tea.Cmd
47 | a.textinput, cmd = a.textinput.Update(msg)
48 | return a, cmd
49 | }
50 |
51 | func (a *shortAnswerField) View() string {
52 | return a.textinput.View()
53 | }
54 |
55 | func (a *shortAnswerField) Focus() tea.Cmd {
56 | return a.textinput.Focus()
57 | }
58 |
59 | func (a *shortAnswerField) SetValue(s string) {
60 | a.textinput.SetValue(s)
61 | }
62 |
63 | func (a *shortAnswerField) Blur() tea.Msg {
64 | return a.textinput.Blur
65 | }
66 |
67 | func (a *shortAnswerField) Value() string {
68 | return a.textinput.Value()
69 | }
70 |
71 | type longAnswerField struct {
72 | textarea textarea.Model
73 | }
74 |
75 | func NewLongAnswerField() *longAnswerField {
76 | a := longAnswerField{}
77 |
78 | model := textarea.New()
79 | model.Placeholder = "Your answer here"
80 | model.Focus()
81 |
82 | a.textarea = model
83 | return &a
84 | }
85 |
86 | func (a *longAnswerField) Blink() tea.Msg {
87 | return textarea.Blink()
88 | }
89 |
90 | func (a *longAnswerField) Init() tea.Cmd {
91 | return nil
92 | }
93 |
94 | func (a *longAnswerField) Update(msg tea.Msg) (Input, tea.Cmd) {
95 | var cmd tea.Cmd
96 | a.textarea, cmd = a.textarea.Update(msg)
97 | return a, cmd
98 | }
99 |
100 | func (a *longAnswerField) View() string {
101 | return a.textarea.View()
102 | }
103 |
104 | func (a *longAnswerField) Focus() tea.Cmd {
105 | return a.textarea.Focus()
106 | }
107 |
108 | func (a *longAnswerField) SetValue(s string) {
109 | a.textarea.SetValue(s)
110 | }
111 |
112 | func (a *longAnswerField) Blur() tea.Msg {
113 | return a.textarea.Blur
114 | }
115 |
116 | func (a *longAnswerField) Value() string {
117 | return a.textarea.Value()
118 | }
119 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 |
8 | tea "github.com/charmbracelet/bubbletea"
9 | "github.com/charmbracelet/lipgloss"
10 | )
11 |
12 | type Styles struct {
13 | BorderColor lipgloss.Color
14 | InputField lipgloss.Style
15 | }
16 |
17 | func DefaultStyles() *Styles {
18 | s := new(Styles)
19 | s.BorderColor = lipgloss.Color("36")
20 | s.InputField = lipgloss.NewStyle().BorderForeground(s.BorderColor).BorderStyle(lipgloss.NormalBorder()).Padding(1).Width(80)
21 | return s
22 | }
23 |
24 | type Main struct {
25 | styles *Styles
26 | index int
27 | questions []Question
28 | width int
29 | height int
30 | done bool
31 | }
32 |
33 | type Question struct {
34 | question string
35 | answer string
36 | input Input
37 | }
38 |
39 | func newQuestion(q string) Question {
40 | return Question{question: q}
41 | }
42 |
43 | func newShortQuestion(q string) Question {
44 | question := newQuestion(q)
45 | model := NewShortAnswerField()
46 | question.input = model
47 | return question
48 | }
49 |
50 | func newLongQuestion(q string) Question {
51 | question := newQuestion(q)
52 | model := NewLongAnswerField()
53 | question.input = model
54 | return question
55 | }
56 |
57 | func New(questions []Question) *Main {
58 | styles := DefaultStyles()
59 | return &Main{styles: styles, questions: questions}
60 | }
61 |
62 | func (m Main) Init() tea.Cmd {
63 | return m.questions[m.index].input.Blink
64 | }
65 |
66 | func (m Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
67 | current := &m.questions[m.index]
68 | var cmd tea.Cmd
69 | switch msg := msg.(type) {
70 | case tea.WindowSizeMsg:
71 | m.width = msg.Width
72 | m.height = msg.Height
73 | case tea.KeyMsg:
74 | switch msg.String() {
75 | case "ctrl+c", "q":
76 | return m, tea.Quit
77 | case "enter":
78 | if m.index == len(m.questions)-1 {
79 | m.done = true
80 | }
81 | current.answer = current.input.Value()
82 | m.Next()
83 | return m, current.input.Blur
84 | }
85 | }
86 | current.input, cmd = current.input.Update(msg)
87 | return m, cmd
88 | }
89 |
90 | func (m Main) View() string {
91 | current := m.questions[m.index]
92 | if m.done {
93 | var output string
94 | for _, q := range m.questions {
95 | output += fmt.Sprintf("%s: %s\n", q.question, q.answer)
96 | }
97 | return output
98 | }
99 | if m.width == 0 {
100 | return "loading..."
101 | }
102 | // stack some left-aligned strings together in the center of the window
103 | return lipgloss.Place(
104 | m.width,
105 | m.height,
106 | lipgloss.Center,
107 | lipgloss.Center,
108 | lipgloss.JoinVertical(
109 | lipgloss.Left,
110 | current.question,
111 | m.styles.InputField.Render(current.input.View()),
112 | ),
113 | )
114 | }
115 |
116 | func (m *Main) Next() {
117 | if m.index < len(m.questions)-1 {
118 | m.index++
119 | } else {
120 | m.index = 0
121 | }
122 | }
123 |
124 | func main() {
125 | // init styles; optional, just showing as a way to organize styles
126 | // start bubble tea and init first model
127 | questions := []Question{newShortQuestion("what is your name?"), newShortQuestion("what is your favourite editor?"), newLongQuestion("what's your favourite quote?")}
128 | main := New(questions)
129 |
130 | f, err := tea.LogToFile("debug.log", "debug")
131 | if err != nil {
132 | fmt.Println("fatal:", err)
133 | os.Exit(1)
134 | }
135 | defer f.Close()
136 | p := tea.NewProgram(*main, tea.WithAltScreen())
137 | if _, err := p.Run(); err != nil {
138 | log.Fatal(err)
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3 | github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
4 | github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
5 | github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og=
6 | github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
7 | github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
8 | github.com/charmbracelet/bubbletea v0.23.1 h1:CYdteX1wCiCzKNUlwm25ZHBIc1GXlYFyUIte8WPvhck=
9 | github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
10 | github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
11 | github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
12 | github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
13 | github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
14 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
15 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
16 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
17 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
18 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
19 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
20 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
21 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
22 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
23 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
24 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
25 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
26 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
27 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
28 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
29 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
30 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
31 | github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
32 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
33 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
34 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
35 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
36 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
37 | github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
38 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
39 | github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
40 | github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
41 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
42 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
43 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
44 | github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
45 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
46 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
47 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
48 | golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
49 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
50 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
51 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
52 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
53 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
54 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
55 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
56 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
57 |
--------------------------------------------------------------------------------