├── .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 | A screenshot of Bashbunni coding 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 | The Charm logo 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 | --------------------------------------------------------------------------------