Skip to content

Commit fe43d26

Browse files
authored
Merge pull request #2035 from onflow/cf/glowup
Add styling to flow root command, results, and test output
2 parents 2bafbd9 + 2070993 commit fe43d26

8 files changed

Lines changed: 244 additions & 69 deletions

File tree

cmd/flow/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ func main() {
121121
Title: "🔗 Dependency Manager",
122122
})
123123

124+
// Initialize template functions for styling
125+
command.InitTemplateFunc(cmd)
124126
cmd.SetUsageTemplate(command.UsageTemplate)
125127

126128
// Don't print usage on error
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,33 @@
1616
* limitations under the License.
1717
*/
1818

19-
package prompt
19+
package branding
2020

2121
import "github.com/charmbracelet/lipgloss"
2222

23-
// Shared color constants for prompts
23+
// Flow brand colors
2424
var (
2525
FlowGreen = lipgloss.Color("#02D87E")
2626
GrayText = lipgloss.Color("8")
2727
PurpleText = lipgloss.Color("#50268C")
28+
ErrorRed = lipgloss.Color("#E55555")
2829
)
2930

30-
// Shared styles for prompts
31+
// Shared styles for consistent branding
3132
var (
3233
GreenStyle = lipgloss.NewStyle().Foreground(FlowGreen)
3334
GrayStyle = lipgloss.NewStyle().Foreground(GrayText)
3435
PurpleStyle = lipgloss.NewStyle().Foreground(PurpleText)
3536
MessageStyle = PurpleStyle
37+
ErrorStyle = lipgloss.NewStyle().Foreground(ErrorRed).Bold(true)
3638
)
39+
40+
// Flow ASCII art logo
41+
const FlowASCII = ` ___ ___
42+
/'___\/\_ \
43+
/\ \__/\//\ \ ___ __ __ __
44+
\ \ ,__\ \ \ \ / __` + "`" + `\/\ \/\ \/\ \
45+
\ \ \_/ \_\ \_/\ \L\ \ \ \_/ \_/ \
46+
\ \_\ /\____\ \____/\ \___x___/'
47+
\/_/ \/____/\/___/ \/__//__/
48+
`

internal/command/result.go

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ import (
3232

3333
"github.com/onflow/flowkit/v2/config"
3434
"github.com/onflow/flowkit/v2/output"
35+
36+
"github.com/onflow/flow-cli/common/branding"
37+
)
38+
39+
// Error styling
40+
var (
41+
errorStyle = branding.ErrorStyle
42+
errorMessageStyle = branding.ErrorStyle
43+
suggestionStyle = branding.GreenStyle.Copy().Italic(true)
44+
descriptionStyle = branding.GrayStyle.Copy().Bold(true)
3545
)
3646

3747
// Result interface describes all the formats for the result output.
@@ -140,35 +150,66 @@ func handleError(description string, err error) {
140150
// handle rpc error
141151
switch t := err.(type) {
142152
case *grpc.RPCError:
143-
_, _ = fmt.Fprintf(os.Stderr, "%s Grpc Error: %s \n", output.ErrorEmoji(), t.GRPCStatus().Err().Error())
153+
errorMsg := errorStyle.Render(fmt.Sprintf("%s Grpc Error:", output.ErrorEmoji()))
154+
detailMsg := errorMessageStyle.Render(t.GRPCStatus().Err().Error())
155+
_, _ = fmt.Fprintf(os.Stderr, "%s %s\n", errorMsg, detailMsg)
144156
default:
145157
if errors.Is(err, config.ErrOutdatedFormat) {
146-
_, _ = fmt.Fprintf(os.Stderr, "%s Config Error: %s \n", output.ErrorEmoji(), err.Error())
147-
_, _ = fmt.Fprintf(os.Stderr, "%s Please reset configuration using: 'flow init --reset'. Read more about new configuration here: https://github.com/onflow/flow-cli/releases/tag/v0.17.0", output.TryEmoji())
158+
errorMsg := errorStyle.Render(fmt.Sprintf("%s Config Error:", output.ErrorEmoji()))
159+
detailMsg := errorMessageStyle.Render(err.Error())
160+
_, _ = fmt.Fprintf(os.Stderr, "%s %s\n", errorMsg, detailMsg)
161+
162+
suggestion := suggestionStyle.Render(fmt.Sprintf("%s Please reset configuration using: 'flow init --reset'. Read more about new configuration here: https://github.com/onflow/flow-cli/releases/tag/v0.17.0", output.TryEmoji()))
163+
_, _ = fmt.Fprintf(os.Stderr, "%s\n", suggestion)
148164
} else if errors.Is(err, config.ErrDoesNotExist) {
149-
_, _ = fmt.Fprintf(os.Stderr, "%s Config Error: %s \n", output.ErrorEmoji(), err.Error())
150-
_, _ = fmt.Fprintf(os.Stderr, "%s Please create configuration using: flow init", output.TryEmoji())
165+
errorMsg := errorStyle.Render(fmt.Sprintf("%s Config Error:", output.ErrorEmoji()))
166+
detailMsg := errorMessageStyle.Render(err.Error())
167+
_, _ = fmt.Fprintf(os.Stderr, "%s %s\n", errorMsg, detailMsg)
168+
169+
suggestion := suggestionStyle.Render(fmt.Sprintf("%s Please create configuration using: flow init", output.TryEmoji()))
170+
_, _ = fmt.Fprintf(os.Stderr, "%s\n", suggestion)
151171
} else if strings.Contains(err.Error(), "transport:") {
152-
_, _ = fmt.Fprintf(os.Stderr, "%s %s \n", output.ErrorEmoji(), strings.Split(err.Error(), "transport:")[1])
153-
_, _ = fmt.Fprintf(os.Stderr, "%s Make sure your emulator is running or connection address is correct.", output.TryEmoji())
172+
errorMsg := errorStyle.Render(fmt.Sprintf("%s Connection Error:", output.ErrorEmoji()))
173+
detailMsg := errorMessageStyle.Render(strings.Split(err.Error(), "transport:")[1])
174+
_, _ = fmt.Fprintf(os.Stderr, "%s %s\n", errorMsg, detailMsg)
175+
176+
suggestion := suggestionStyle.Render(fmt.Sprintf("%s Make sure your emulator is running or connection address is correct.", output.TryEmoji()))
177+
_, _ = fmt.Fprintf(os.Stderr, "%s\n", suggestion)
154178
} else if strings.Contains(err.Error(), "NotFound desc =") {
155-
_, _ = fmt.Fprintf(os.Stderr, "%s Not Found:%s \n", output.ErrorEmoji(), strings.Split(err.Error(), "NotFound desc =")[1])
179+
errorMsg := errorStyle.Render(fmt.Sprintf("%s Not Found:", output.ErrorEmoji()))
180+
detailMsg := errorMessageStyle.Render(strings.Split(err.Error(), "NotFound desc =")[1])
181+
_, _ = fmt.Fprintf(os.Stderr, "%s%s\n", errorMsg, detailMsg)
156182
} else if strings.Contains(err.Error(), "code = InvalidArgument desc = ") {
157183
desc := strings.Split(err.Error(), "code = InvalidArgument desc = ")
158-
_, _ = fmt.Fprintf(os.Stderr, "%s Invalid argument: %s \n", output.ErrorEmoji(), desc[len(desc)-1])
184+
errorMsg := errorStyle.Render(fmt.Sprintf("%s Invalid argument:", output.ErrorEmoji()))
185+
detailMsg := errorMessageStyle.Render(desc[len(desc)-1])
186+
_, _ = fmt.Fprintf(os.Stderr, "%s %s\n", errorMsg, detailMsg)
187+
159188
if strings.Contains(err.Error(), "is invalid for chain") {
160-
_, _ = fmt.Fprintf(os.Stderr, "%s Check you are connecting to the correct network or account address you use is correct.", output.TryEmoji())
189+
suggestion := suggestionStyle.Render(fmt.Sprintf("%s Check you are connecting to the correct network or account address you use is correct.", output.TryEmoji()))
190+
_, _ = fmt.Fprintf(os.Stderr, "%s\n", suggestion)
161191
} else {
162-
_, _ = fmt.Fprintf(os.Stderr, "%s Check your argument and flags value, you can use --help.", output.TryEmoji())
192+
suggestion := suggestionStyle.Render(fmt.Sprintf("%s Check your argument and flags value, you can use --help.", output.TryEmoji()))
193+
_, _ = fmt.Fprintf(os.Stderr, "%s\n", suggestion)
163194
}
164195
} else if strings.Contains(err.Error(), "invalid signature:") {
165-
_, _ = fmt.Fprintf(os.Stderr, "%s Invalid signature: %s \n", output.ErrorEmoji(), strings.Split(err.Error(), "invalid signature:")[1])
166-
_, _ = fmt.Fprintf(os.Stderr, "%s Check the signer private key is provided or is in the correct format. If running emulator, make sure it's using the same configuration as this command.", output.TryEmoji())
196+
errorMsg := errorStyle.Render(fmt.Sprintf("%s Invalid signature:", output.ErrorEmoji()))
197+
detailMsg := errorMessageStyle.Render(strings.Split(err.Error(), "invalid signature:")[1])
198+
_, _ = fmt.Fprintf(os.Stderr, "%s %s\n", errorMsg, detailMsg)
199+
200+
suggestion := suggestionStyle.Render(fmt.Sprintf("%s Check the signer private key is provided or is in the correct format. If running emulator, make sure it's using the same configuration as this command.", output.TryEmoji()))
201+
_, _ = fmt.Fprintf(os.Stderr, "%s\n", suggestion)
167202
} else if strings.Contains(err.Error(), "signature could not be verified using public key with") {
168-
_, _ = fmt.Fprintf(os.Stderr, "%s %s: %s \n", output.ErrorEmoji(), description, err)
169-
_, _ = fmt.Fprintf(os.Stderr, "%s If you are running emulator locally make sure that the emulator was started with the same config as used in this command. \nTry restarting the emulator.", output.TryEmoji())
203+
errorMsg := errorStyle.Render(fmt.Sprintf("%s %s:", output.ErrorEmoji(), description))
204+
detailMsg := errorMessageStyle.Render(err.Error())
205+
_, _ = fmt.Fprintf(os.Stderr, "%s %s\n", errorMsg, detailMsg)
206+
207+
suggestion := suggestionStyle.Render(fmt.Sprintf("%s If you are running emulator locally make sure that the emulator was started with the same config as used in this command. \nTry restarting the emulator.", output.TryEmoji()))
208+
_, _ = fmt.Fprintf(os.Stderr, "%s\n", suggestion)
170209
} else {
171-
_, _ = fmt.Fprintf(os.Stderr, "%s %s: %s", output.ErrorEmoji(), description, err)
210+
errorMsg := errorStyle.Render(fmt.Sprintf("%s %s:", output.ErrorEmoji(), description))
211+
detailMsg := errorMessageStyle.Render(err.Error())
212+
_, _ = fmt.Fprintf(os.Stderr, "%s %s\n", errorMsg, detailMsg)
172213
}
173214
}
174215

internal/command/template.go

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,111 @@
1818

1919
package command
2020

21-
var UsageTemplate = `Usage:{{if .Runnable}}
21+
import (
22+
"fmt"
23+
"text/template"
24+
25+
"github.com/charmbracelet/lipgloss"
26+
"github.com/spf13/cobra"
27+
28+
"github.com/onflow/flow-cli/common/branding"
29+
)
30+
31+
var (
32+
// Header styles
33+
logoStyle = lipgloss.NewStyle().
34+
Foreground(branding.FlowGreen).
35+
Bold(true)
36+
37+
welcomeStyle = lipgloss.NewStyle().
38+
Foreground(branding.PurpleText).
39+
Bold(true)
40+
41+
subtitleStyle = lipgloss.NewStyle().
42+
Foreground(branding.GrayText).
43+
Italic(true).
44+
MarginLeft(2)
45+
46+
// Command group styles
47+
groupTitleStyle = lipgloss.NewStyle().
48+
Foreground(branding.FlowGreen).
49+
Bold(true)
50+
51+
commandNameStyle = lipgloss.NewStyle().
52+
Foreground(branding.PurpleText).
53+
Bold(true)
54+
55+
commandDescStyle = lipgloss.NewStyle().
56+
Foreground(lipgloss.Color("7"))
57+
58+
// Section styles
59+
sectionTitleStyle = lipgloss.NewStyle().
60+
Foreground(branding.FlowGreen).
61+
Bold(true)
62+
63+
// Footer style
64+
footerStyle = lipgloss.NewStyle().
65+
Foreground(branding.GrayText).
66+
Italic(true)
67+
)
68+
69+
// Template functions for styling
70+
var templateFuncs = template.FuncMap{
71+
"styleFlowHeader": func() string {
72+
logo := logoStyle.Render(branding.FlowASCII)
73+
welcome := welcomeStyle.Render("👋 Welcome Flow developer!")
74+
subtitle := subtitleStyle.Render("If you are starting a new flow project use our super commands, start by running 'flow init'.")
75+
return fmt.Sprintf("%s\n%s\n%s\n", logo, welcome, subtitle)
76+
},
77+
"styleGroupTitle": func(title string) string {
78+
return groupTitleStyle.Render(title)
79+
},
80+
"styleCommandName": func(name string) string {
81+
return commandNameStyle.Render(name)
82+
},
83+
"styleCommandDesc": func(desc string) string {
84+
return commandDescStyle.Render(desc)
85+
},
86+
"styleSectionTitle": func(title string) string {
87+
return sectionTitleStyle.Render(title)
88+
},
89+
"styleFooter": func(text string) string {
90+
return footerStyle.Render(text)
91+
},
92+
}
93+
94+
func InitTemplateFunc(cmd *cobra.Command) {
95+
for name, fn := range templateFuncs {
96+
cobra.AddTemplateFunc(name, fn)
97+
}
98+
}
99+
100+
var UsageTemplate = `{{if (eq .Name "flow")}}{{styleFlowHeader}}{{else}}Usage:{{if .Runnable}}
22101
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
23102
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
24103
25104
Aliases:
26105
{{.NameAndAliases}}{{end}}{{if .HasExample}}
27106
28107
Examples:
29-
{{.Example}}{{end}}
30-
{{if .HasAvailableSubCommands}}{{if (eq .Name "flow")}}
31-
👋 Welcome Flow developer!
32-
If you are starting a new flow project use our super commands, start by running 'flow init'. {{end}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
33-
Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
34-
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
108+
{{.Example}}{{end}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
109+
110+
{{styleSectionTitle "Available Commands:"}}{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
111+
{{styleCommandName (rpad .Name .NamePadding)}} {{styleCommandDesc .Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
35112
36-
[1m{{.Title}}[0m{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
37-
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
113+
{{styleGroupTitle .Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
114+
{{styleCommandName (rpad .Name .NamePadding)}} {{styleCommandDesc .Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
38115
39-
Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
40-
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
116+
{{styleSectionTitle "Additional Commands:"}}{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
117+
{{styleCommandName (rpad .Name .NamePadding)}} {{styleCommandDesc .Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
41118
42-
Flags:
119+
{{styleSectionTitle "Flags:"}}
43120
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
44121
45-
Global Flags:
122+
{{styleSectionTitle "Global Flags:"}}
46123
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
47124
48-
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
49-
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
125+
{{styleSectionTitle "Additional help topics:"}}{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
126+
{{styleCommandName (rpad .CommandPath .CommandPathPadding)}} {{styleCommandDesc .Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
50127
51-
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
52-
`
128+
{{styleFooter (printf "Use \"%s [command] --help\" for more information about a command." .CommandPath)}}{{end}}`

internal/prompt/select.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525

2626
tea "github.com/charmbracelet/bubbletea"
2727
"golang.org/x/term"
28+
29+
"github.com/onflow/flow-cli/common/branding"
2830
)
2931

3032
// optionSelectModel represents the prompt state but is now private
@@ -83,24 +85,24 @@ func (m optionSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8385

8486
func (m optionSelectModel) View() string {
8587
var b strings.Builder
86-
b.WriteString(fmt.Sprintf("%s\n", MessageStyle.Render(m.message)))
87-
b.WriteString(GrayStyle.Render("Use arrow keys to navigate, space to select, enter to confirm or skip, q to quit:") + "\n\n")
88+
b.WriteString(fmt.Sprintf("%s\n", branding.MessageStyle.Render(m.message)))
89+
b.WriteString(branding.GrayStyle.Render("Use arrow keys to navigate, space to select, enter to confirm or skip, q to quit:") + "\n\n")
8890
for i, choice := range m.choices {
8991
if m.cursor == i {
90-
b.WriteString(GreenStyle.Render("> "))
92+
b.WriteString(branding.GreenStyle.Render("> "))
9193
} else {
9294
b.WriteString(" ")
9395
}
9496
// Mark selected items
9597
if _, ok := m.selected[i]; ok {
96-
b.WriteString(GreenStyle.Render("[x] "))
98+
b.WriteString(branding.GreenStyle.Render("[x] "))
9799
} else {
98100
b.WriteString("[ ] ")
99101
}
100102

101103
// Style the choice text if it's selected
102104
if m.cursor == i {
103-
b.WriteString(GreenStyle.Render(choice) + "\n")
105+
b.WriteString(branding.GreenStyle.Render(choice) + "\n")
104106
} else {
105107
b.WriteString(choice + "\n")
106108
}
@@ -180,19 +182,19 @@ func (m singleSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
180182

181183
func (m singleSelectModel) View() string {
182184
var b strings.Builder
183-
b.WriteString(fmt.Sprintf("%s\n\n", MessageStyle.Render(m.message)))
185+
b.WriteString(fmt.Sprintf("%s\n\n", branding.MessageStyle.Render(m.message)))
184186

185187
for i, choice := range m.choices {
186188
if m.cursor == i {
187-
b.WriteString(GreenStyle.Render("> "))
188-
b.WriteString(GreenStyle.Render(choice) + "\n")
189+
b.WriteString(branding.GreenStyle.Render("> "))
190+
b.WriteString(branding.GreenStyle.Render(choice) + "\n")
189191
} else {
190192
b.WriteString(" ")
191193
b.WriteString(choice + "\n")
192194
}
193195
}
194196

195-
b.WriteString("\n" + GrayStyle.Render("Use arrow keys to navigate, enter to select, esc to cancel"))
197+
b.WriteString("\n" + branding.GrayStyle.Render("Use arrow keys to navigate, enter to select, esc to cancel"))
196198
return b.String()
197199
}
198200

internal/prompt/textinput.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ import (
2525
"github.com/charmbracelet/bubbles/textinput"
2626
tea "github.com/charmbracelet/bubbletea"
2727
"golang.org/x/term"
28+
29+
"github.com/onflow/flow-cli/common/branding"
2830
)
2931

3032
var (
31-
projectNameStyle = MessageStyle // Use default color for messages
32-
helpTextStyle = GrayStyle
33+
projectNameStyle = branding.MessageStyle // Use default color for messages
34+
helpTextStyle = branding.GrayStyle
3335
)
3436

3537
// textInputModel is now private, only accessible within the 'prompt' package.
@@ -51,8 +53,8 @@ func newTextInput(customMsg, placeholder, defaultValue string, validate func(str
5153
ti.Width = 30
5254

5355
// Style the text input with green cursor and text
54-
ti.TextStyle = GreenStyle
55-
ti.Cursor.Style = GreenStyle
56+
ti.TextStyle = branding.GreenStyle
57+
ti.Cursor.Style = branding.GreenStyle
5658

5759
if defaultValue != "" {
5860
ti.SetValue(defaultValue)

0 commit comments

Comments
 (0)