Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions cmd/microcloud/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (c *initConfig) askRetry(question string, f func() error) error {
return err
}

fmt.Printf("%s %s\n", tui.ErrorSymbol(), tui.ErrorColor(err.Error(), true))
tui.PrintError(err.Error())
retry, err = c.asker.AskBool(question, true)
if err != nil {
return err
Expand Down Expand Up @@ -645,7 +645,7 @@ func (c *initConfig) askRemotePool(sh *service.Handler) error {
}

if availableDiskCount == 0 {
fmt.Println(tui.WarningColor("Warning: No disks available for distributed storage. Skipping configuration", false))
tui.PrintWarning("No disks available for distributed storage. Skipping configuration")

return nil
}
Expand Down Expand Up @@ -1125,7 +1125,7 @@ func (c *initConfig) askOVNNetwork(sh *service.Handler) error {
canOVNUnderlay := true
for peer, system := range c.systems {
if len(c.state[system.ServerInfo.Name].AvailableOVNInterfaces) == 0 {
fmt.Println(tui.WarningColor(fmt.Sprintf("Not enough interfaces available on %s to create an underlay network, skipping", peer), false))
tui.PrintWarning(fmt.Sprintf("Not enough interfaces available on %s to create an underlay network. Skipping configuration", peer))
canOVNUnderlay = false
break
}
Expand Down Expand Up @@ -1323,7 +1323,7 @@ func (c *initConfig) askCephNetwork(sh *service.Handler) error {
availableCephNetworkInterfaces := map[string]map[string]service.DedicatedInterface{}
for name, state := range c.state {
if len(state.AvailableCephInterfaces) == 0 {
fmt.Println(tui.WarningColor(fmt.Sprintf("No network interfaces found with IPs on %q to set a dedicated Ceph network, skipping Ceph network setup", name), false))
tui.PrintWarning(fmt.Sprintf("No network interfaces found with IPs on %q to set up a dedicated Ceph network. Skipping Ceph network setup", name))

return nil
}
Expand Down Expand Up @@ -1657,7 +1657,7 @@ func (c *initConfig) askJoinConfirmation(gw *cloudClient.WebsocketGateway, servi
fmt.Println(tui.SummarizeResult("Received confirmation from system %s", session.Intent.Name))
fmt.Println("")
fmt.Println(tui.Note(tui.Yellow, tui.WarningSymbol()+tui.SetColor(tui.Bright, " Do not exit out to keep the session alive", true)) + "\n")
fmt.Println(tui.Printf(tui.Fmt{Arg: "Complete the remaining configuration on %s ..."}, tui.Fmt{Arg: session.Intent.Name, Color: tui.Yellow, Bold: true}))
fmt.Println(tui.Printf(tui.Fmt{Arg: "Complete the remaining configuration on %s ..."}, tui.Fmt{Arg: session.Intent.Name, Bold: true}))
}

err = gw.ReceiveWithContext(gw.Context(), &session)
Expand Down
3 changes: 2 additions & 1 deletion cmd/microcloud/preseed.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/canonical/microcloud/microcloud/api"
"github.com/canonical/microcloud/microcloud/api/types"
cloudClient "github.com/canonical/microcloud/microcloud/client"
"github.com/canonical/microcloud/microcloud/cmd/tui"
"github.com/canonical/microcloud/microcloud/multicast"
"github.com/canonical/microcloud/microcloud/service"
)
Expand Down Expand Up @@ -695,7 +696,7 @@ func (p *Preseed) Parse(s *service.Handler, c *initConfig, installedServices map

for serviceType, cluster := range existingClusters {
if len(cluster) > 0 {
fmt.Printf("Existing %s cluster is incompatible with MicroCloud, skipping %s setup\n", serviceType, serviceType)
tui.PrintWarning(fmt.Sprintf("Existing %s cluster is incompatible with MicroCloud. Skipping %s setup", serviceType, serviceType))

delete(s.Services, serviceType)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/microcloud/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ func (c *initConfig) joiningSession(gw *cloudClient.WebsocketGateway, sh *servic
fmt.Printf("\n%s %s\n\n", tmpl, fingerprintArg)

tmplArg := tui.Fmt{Arg: "Select %s on %s to let it join the cluster"}
localArg := tui.Fmt{Arg: sh.Name, Color: tui.Yellow, Bold: true}
remoteArg := tui.Fmt{Arg: session.InitiatorName, Color: tui.Yellow, Bold: true}
localArg := tui.Fmt{Arg: sh.Name, Bold: true}
remoteArg := tui.Fmt{Arg: session.InitiatorName, Bold: true}
fmt.Println(tui.Printf(tmplArg, localArg, remoteArg))
}

Expand Down
75 changes: 59 additions & 16 deletions cmd/tui/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import (
"bufio"
"fmt"
"os"
"slices"
"strings"
"sync"

"github.com/canonical/lxd/shared/cmd"
tea "github.com/charmbracelet/bubbletea"
)

// ContextError is the charmbracelet representation of a context cancellation error.
var ContextError error = tea.ErrProgramKilled

// InvalidInputError is used to indicate false input to an asked question.
var InvalidInputError func() = func() { PrintError("Invalid input, try again") }

// InputHandler handles input dialogs.
type InputHandler struct {
input *os.File
Expand Down Expand Up @@ -58,11 +61,6 @@ func (i *InputHandler) countAllRows() int {
return i.table.countRawRows()
}

// printWarning prints the given warning with "!" appended to the front of the message.
func (i *InputHandler) printWarning(warning string) {
fmt.Printf("%s %s\n", WarningSymbol(), warning)
}

// formatQuestion enriches the plain question string with default and accepted answers.
func (i *InputHandler) formatQuestion(question string, defaultAnswer string, acceptedAnswers []string) string {
var acceptedAnswersBlock string
Expand All @@ -78,9 +76,27 @@ func (i *InputHandler) formatQuestion(question string, defaultAnswer string, acc
return fmt.Sprintf("%s%s%s: ", question, acceptedAnswersBlock, defaultAnswerBlock)
}

// Ask a question on the output stream and read the answer from the input stream.
func (i *InputHandler) askQuestion(question, defaultAnswer string) (string, error) {
fmt.Print(question)

return i.readAnswer(defaultAnswer)
}

// Read the user's answer from the input stream, trimming newline and providing a default.
func (i *InputHandler) readAnswer(defaultAnswer string) (string, error) {
answer, err := bufio.NewReader(i.input).ReadString('\n')
answer = strings.TrimSpace(strings.TrimSuffix(answer, "\n"))
if answer == "" {
answer = defaultAnswer
}

return answer, err
}

// AskBoolWarn is the same as AskBool but it prints the given warning before asking.
func (i *InputHandler) AskBoolWarn(warning string, question string, defaultAnswer bool) (bool, error) {
i.printWarning(warning)
PrintWarning(warning)
return i.AskBool(question, defaultAnswer)
}

Expand All @@ -93,13 +109,25 @@ func (i *InputHandler) AskBool(question string, defaultAnswer bool) (bool, error
defaultAnswerStr = "yes"
}

asker := cmd.NewAsker(bufio.NewReader(i.input), nil)
return asker.AskBool(i.formatQuestion(question, defaultAnswerStr, []string{"yes", "no"}), defaultAnswerStr)
for {
answer, err := i.askQuestion(i.formatQuestion(question, defaultAnswerStr, []string{"yes", "no"}), defaultAnswerStr)
if err != nil {
return false, err
}

if slices.Contains([]string{"yes", "y"}, strings.ToLower(answer)) {
return true, nil
} else if slices.Contains([]string{"no", "n"}, strings.ToLower(answer)) {
return false, nil
}

InvalidInputError()
}
}

// AskStringWarn is the same as AskString but it prints the given warning before asking.
func (i *InputHandler) AskStringWarn(warning string, question string, defaultAnswer string, validator func(string) error) (string, error) {
i.printWarning(warning)
PrintWarning(warning)
return i.AskString(question, defaultAnswer, validator)
}

Expand All @@ -108,11 +136,26 @@ func (i *InputHandler) AskString(question string, defaultAnswer string, validato
i.setActive(true)
defer i.setActive(false)

asker := cmd.NewAsker(bufio.NewReader(i.input), nil)
result, err := asker.AskString(i.formatQuestion(question, defaultAnswer, nil), defaultAnswer, validator)
if err != nil {
return "", err
}
for {
answer, err := i.askQuestion(i.formatQuestion(question, defaultAnswer, nil), defaultAnswer)
if err != nil {
return "", err
}

return result, nil
if validator != nil {
err = validator(answer)
if err != nil {
InvalidInputError()
continue
}

return answer, err
}

if len(answer) != 0 {
return answer, err
}

InvalidInputError()
}
}
2 changes: 1 addition & 1 deletion cmd/tui/selectable_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ type selectableTable struct {
func SummarizeResult(tmpl string, args ...any) string {
fmtArgs := []Fmt{}
for _, arg := range args {
fmtArgs = append(fmtArgs, Fmt{Arg: arg, Color: Yellow, Bold: true})
fmtArgs = append(fmtArgs, Fmt{Arg: arg, Bold: true})
}

return Printf(Fmt{Arg: " " + tmpl, Color: White}, fmtArgs...)
Expand Down
22 changes: 20 additions & 2 deletions cmd/tui/style.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,30 @@ type ColorErr struct{}

// Write colors the given error red and sends it to os.Stderr.
func (*ColorErr) Write(p []byte) (n int, err error) {
return os.Stderr.WriteString(ErrorColor(strings.TrimSpace(string(p)), false) + "\n")
trimmedErr := strings.TrimSpace(string(p))

// Cobra allows setting the error prefix using SetErrPrefix() func but
// it ignores the setting when assigning an empty string.
// Therefore we can only trim the prefix to be able to set our own
// customized error prefix using tui styling.
withoutPrefixErr := strings.TrimPrefix(trimmedErr, "Error: ")

return os.Stderr.WriteString(SprintError(withoutPrefixErr))
}

// PrintWarning calls Println but it appends "! Warning:" to the front of the message.
func PrintWarning(s string) {
fmt.Printf("%s %s: %s\n", WarningSymbol(), WarningColor("Warning", true), s)
fmt.Println(WarningSymbol(), WarningColor("Warning:", true), WarningColor(s, false))
}

// SprintError crafts the error string without writing it to any output yet.
func SprintError(s string) string {
return fmt.Sprintln(ErrorSymbol(), ErrorColor("Error:", true), ErrorColor(s, false))
}

// PrintError calls Println but it appends "⨯ Error:" to the front of the message.
func PrintError(s string) {
fmt.Print(SprintError(s))
}

// Fmt represents the data supplied to ColorPrintf. In particular, it takes a color to apply to the text, and the text itself.
Expand Down
2 changes: 1 addition & 1 deletion service/lxd.go
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ func (s *LXDService) ValidateCephInterfaces(cephNetworkSubnetStr string, peerInt
}

if len(data) == 0 {
fmt.Println(tui.WarningColor("No network interfaces found with IPs in the specified subnet, skipping Ceph network setup", false))
tui.PrintWarning("No network interfaces found with IPs in the specified subnet. Skipping Ceph network setup")
}

return data, nil
Expand Down
2 changes: 1 addition & 1 deletion service/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func validateVersion(serviceType types.ServiceType, daemonVersion string) error

// Print a warning in case a non-LTS version of LXD is used.
if semver.Compare(semver.MajorMinor(lxdVersion), semver.MajorMinor(expectedVersion)) == 1 {
fmt.Printf("%s Discovered non-LTS version %q of LXD\n", tui.WarningSymbol(), daemonVersion)
tui.PrintWarning(fmt.Sprintf("Discovered non-LTS version %q of LXD", daemonVersion))
}

case types.MicroOVN:
Expand Down
Loading