11package main
22
33import (
4- "fmt "
4+ "errors "
55 "strings"
6+ "time"
7+
8+ "github.com/charmbracelet/bubbles/spinner"
69
710 "github.com/charmbracelet/bubbles/textinput"
811 tea "github.com/charmbracelet/bubbletea"
@@ -56,12 +59,32 @@ var (
5659 Background (lipgloss.AdaptiveColor {Light : "#DDDDDD" , Dark : "#626262" }).
5760 Padding (0 , 1 , 0 , 1 ).
5861 Bold (true )
62+
63+ inputsErrLayout = lipgloss .NewStyle ().
64+ Padding (0 , 0 , 0 , 1 )
65+
66+ inputsErrStyle = lipgloss .NewStyle ().
67+ Foreground (lipgloss .Color ("#FFFDF5" )).
68+ Background (lipgloss .Color ("#FF6037" )).
69+ Padding (0 , 1 , 0 , 1 ).
70+ Bold (true )
71+
72+ spinnerMetaFrame1 = lipgloss .NewStyle ().Foreground (lipgloss .Color ("1" )).Render ("❯" )
73+ spinnerMetaFrame2 = lipgloss .NewStyle ().Foreground (lipgloss .Color ("3" )).Render ("❯" )
74+ spinnerMetaFrame3 = lipgloss .NewStyle ().Foreground (lipgloss .Color ("2" )).Render ("❯" )
5975)
6076
77+ type inputWithCheck struct {
78+ input textinput.Model
79+ checker func (s string ) error
80+ }
81+
6182type inputsModel struct {
6283 focusIndex int
6384 title string
64- inputs []textinput.Model
85+ inputs []inputWithCheck
86+ err error
87+ errSpinner spinner.Model
6588}
6689
6790func (m inputsModel ) Init () tea.Cmd {
@@ -79,8 +102,19 @@ func (m inputsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
79102 case "tab" , "shift+tab" , "enter" , "up" , "down" :
80103 s := msg .String ()
81104
82- if s == "enter" && m .focusIndex == len (m .inputs ) {
83- return m , func () tea.Msg { return done {nextView : COMMIT } }
105+ if s == "enter" {
106+ if m .focusIndex == len (m .inputs ) {
107+ for _ , iwc := range m .inputs {
108+ if iwc .checker != nil {
109+ m .err = iwc .checker (iwc .input .Value ())
110+ if m .err != nil {
111+ return m , spinner .Tick
112+ }
113+ }
114+ }
115+ return m , func () tea.Msg { return done {nextView : COMMIT } }
116+ }
117+
84118 }
85119
86120 // Cycle indexes
@@ -100,22 +134,26 @@ func (m inputsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
100134 for i := 0 ; i <= len (m .inputs )- 1 ; i ++ {
101135 if i == m .focusIndex {
102136 // Set focused state
103- cmds [i ] = m .inputs [i ].Focus ()
104- m .inputs [i ].PromptStyle = inputsPromptStyle
105- m .inputs [i ].TextStyle = inputsTextStyle
137+ cmds [i ] = m .inputs [i ].input . Focus ()
138+ m .inputs [i ].input . PromptStyle = inputsPromptStyle
139+ m .inputs [i ].input . TextStyle = inputsTextStyle
106140 continue
107141 }
108142 // Remove focused state
109- m .inputs [i ].Blur ()
110- m .inputs [i ].PromptStyle = inputsPromptNormalStyle
111- m .inputs [i ].TextStyle = inputsTextNormalStyle
143+ m .inputs [i ].input . Blur ()
144+ m .inputs [i ].input . PromptStyle = inputsPromptNormalStyle
145+ m .inputs [i ].input . TextStyle = inputsTextNormalStyle
112146 }
113147
114148 return m , tea .Batch (cmds ... )
115149 }
116150 case string :
117151 m .title = "✔ Commit Type: " + msg
118152 return m , nil
153+ case spinner.TickMsg :
154+ var cmd tea.Cmd
155+ m .errSpinner , cmd = m .errSpinner .Update (msg )
156+ return m , cmd
119157 }
120158
121159 // Handle character input and blinking
@@ -125,12 +163,17 @@ func (m inputsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
125163}
126164
127165func (m * inputsModel ) updateInputs (msg tea.Msg ) tea.Cmd {
128- var cmds = make ([]tea.Cmd , len (m .inputs ))
166+ var cmds = make ([]tea.Cmd , len (m .inputs )+ 1 )
129167
130168 // Only text inputs with Focus() set will respond, so it's safe to simply
131169 // update all of them here without any further logic.
132170 for i := range m .inputs {
133- m .inputs [i ], cmds [i ] = m .inputs [i ].Update (msg )
171+ m .inputs [i ].input , cmds [i ] = m .inputs [i ].input .Update (msg )
172+ }
173+
174+ if m .focusIndex < len (m .inputs ) && m .inputs [m .focusIndex ].checker != nil {
175+ m .err = m .inputs [m .focusIndex ].checker (m .inputs [m .focusIndex ].input .Value ())
176+ cmds [len (m .inputs )] = spinner .Tick
134177 }
135178
136179 return tea .Batch (cmds ... )
@@ -140,7 +183,7 @@ func (m inputsModel) View() string {
140183 var b strings.Builder
141184
142185 for i := range m .inputs {
143- b .WriteString (m .inputs [i ].View ())
186+ b .WriteString (m .inputs [i ].input . View ())
144187 if i < len (m .inputs )- 1 {
145188 b .WriteRune ('\n' )
146189 }
@@ -150,7 +193,12 @@ func (m inputsModel) View() string {
150193 if m .focusIndex == len (m .inputs ) {
151194 button = inputsButtonStyle .Render ("➜ Submit" )
152195 }
153- _ , _ = fmt .Fprint (& b , inputsButtonBlockStyle .Render (button ))
196+
197+ if m .err != nil {
198+ b .WriteString (inputsButtonBlockStyle .Render (button + inputsErrLayout .Render (m .errSpinner .View ()+ " " + inputsErrStyle .Render (m .err .Error ()))))
199+ } else {
200+ b .WriteString (inputsButtonBlockStyle .Render (button ))
201+ }
154202
155203 title := inputsTitleBarStyle .Render (inputsTitleStyle .Render (m .title ))
156204 inputs := inputsBlockStyle .Render (b .String ())
@@ -160,37 +208,69 @@ func (m inputsModel) View() string {
160208
161209func newInputsModel () inputsModel {
162210 m := inputsModel {
163- inputs : make ([]textinput. Model , 4 ),
211+ inputs : make ([]inputWithCheck , 4 ),
164212 }
165213
166- var t textinput.Model
167214 for i := range m .inputs {
168- t = textinput .NewModel ()
169- t .CursorStyle = inputsCursorStyle
170- t .CharLimit = 128
215+ var iwc inputWithCheck
216+
217+ iwc .input = textinput .NewModel ()
218+ iwc .input .CursorStyle = inputsCursorStyle
219+ iwc .input .CharLimit = 128
171220
172221 switch i {
173222 case 0 :
174- t .Prompt = "1. SCOPE "
175- t .Placeholder = "Specifying place of the commit change."
176- t .PromptStyle = inputsPromptStyle
177- t .TextStyle = inputsTextStyle
178- t .Focus ()
223+ iwc .input .Prompt = "1. SCOPE "
224+ iwc .input .Placeholder = "Specifying place of the commit change."
225+ iwc .input .PromptStyle = inputsPromptStyle
226+ iwc .input .TextStyle = inputsTextStyle
227+ iwc .input .Focus ()
228+ iwc .checker = func (s string ) error {
229+ if strings .TrimSpace (s ) == "" {
230+ return errors .New ("Scope cannot be empty" )
231+ }
232+ return nil
233+ }
179234 case 1 :
180- t .Prompt = "2. SUBJECT "
181- t .PromptStyle = inputsPromptNormalStyle
182- t .Placeholder = "A very short description of the change."
235+ iwc .input .Prompt = "2. SUBJECT "
236+ iwc .input .PromptStyle = inputsPromptNormalStyle
237+ iwc .input .Placeholder = "A very short description of the change."
238+ iwc .checker = func (s string ) error {
239+ if strings .TrimSpace (s ) == "" {
240+ return errors .New ("Subject cannot be empty" )
241+ }
242+ return nil
243+ }
183244 case 2 :
184- t .Prompt = "3. BODY "
185- t .PromptStyle = inputsPromptNormalStyle
186- t .Placeholder = "Motivation and contrasts for the change."
245+ iwc . input .Prompt = "3. BODY "
246+ iwc . input .PromptStyle = inputsPromptNormalStyle
247+ iwc . input .Placeholder = "Motivation and contrasts for the change."
187248 case 3 :
188- t .Prompt = "4. FOOTER "
189- t .PromptStyle = inputsPromptNormalStyle
190- t .Placeholder = "Description of the change, justification and migration notes."
249+ iwc . input .Prompt = "4. FOOTER "
250+ iwc . input .PromptStyle = inputsPromptNormalStyle
251+ iwc . input .Placeholder = "Description of the change, justification and migration notes."
191252 }
192253
193- m .inputs [i ] = t
254+ m .inputs [i ] = iwc
255+ }
256+
257+ m .errSpinner = spinner .NewModel ()
258+ m .errSpinner .Spinner = spinner.Spinner {
259+ Frames : []string {
260+ // "❯ "
261+ spinnerMetaFrame1 + " " ,
262+ // "❯❯ "
263+ spinnerMetaFrame1 + spinnerMetaFrame2 + " " ,
264+ // "❯❯❯ "
265+ spinnerMetaFrame1 + spinnerMetaFrame2 + spinnerMetaFrame3 + " " ,
266+ // " ❯❯❯"
267+ " " + spinnerMetaFrame1 + spinnerMetaFrame2 + spinnerMetaFrame3 ,
268+ // " ❯❯"
269+ " " + spinnerMetaFrame1 + spinnerMetaFrame2 ,
270+ // " ❯"
271+ " " + spinnerMetaFrame1 ,
272+ },
273+ FPS : time .Second / 10 ,
194274 }
195275
196276 return m
0 commit comments