diff --git a/components/playground/main.go b/components/playground/main.go index fb94016bab..8215362fed 100644 --- a/components/playground/main.go +++ b/components/playground/main.go @@ -42,6 +42,7 @@ import ( logprinter "github.com/pingcap/tiup/pkg/logger/printer" "github.com/pingcap/tiup/pkg/repository" "github.com/pingcap/tiup/pkg/telemetry" + "github.com/pingcap/tiup/pkg/tui/colorstr" "github.com/pingcap/tiup/pkg/utils" "github.com/pingcap/tiup/pkg/version" "github.com/spf13/cobra" @@ -235,19 +236,23 @@ Examples: if !semver.IsValid(options.Version) { version, err := env.V1Repository().ResolveComponentVersion(spec.ComponentTiDB, options.Version) if err != nil { - return errors.Annotate(err, fmt.Sprintf("can not expand version %s to a valid semver string", options.Version)) + return errors.Annotate(err, fmt.Sprintf("Cannot resolve version %s to a valid semver string", options.Version)) } // for nightly, may not use the same version for cluster if options.Version == "nightly" { version = "nightly" } - fmt.Println(color.YellowString(`Using the version %s for version constraint "%s". -If you'd like to use a TiDB version other than %s, cancel and retry with the following arguments: - Specify version manually: tiup playground - Specify version range: tiup playground ^5 - The nightly version: tiup playground nightly -`, version, options.Version, version)) + if options.Version != version.String() { + colorstr.Fprintf(os.Stderr, ` +Note: Version constraint [bold]%s[reset] is resolved to [green][bold]%s[reset]. If you'd like to use other versions: + + Use exact version: [tiup_command]tiup playground v7.1.0[reset] + Use version range: [tiup_command]tiup playground ^5[reset] + Use nightly: [tiup_command]tiup playground nightly[reset] + +`, options.Version, version.String()) + } options.Version = version.String() } diff --git a/go.mod b/go.mod index 7e31a583a6..b6741615a1 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/juju/ansiterm v1.0.0 github.com/mattn/go-runewidth v0.0.14 github.com/minio/minio-go/v7 v7.0.52 + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db github.com/otiai10/copy v1.9.0 github.com/pelletier/go-toml v1.9.5 github.com/pingcap/check v0.0.0-20211026125417-57bd13f7b5f0 diff --git a/go.sum b/go.sum index a12f529e22..6d8661679d 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,8 @@ github.com/minio/minio-go/v7 v7.0.52 h1:8XhG36F6oKQUDDSuz6dY3rioMzovKjW40W6ANuN0 github.com/minio/minio-go/v7 v7.0.52/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/pkg/exec/run.go b/pkg/exec/run.go index ed5fb136e8..0ca74180ca 100644 --- a/pkg/exec/run.go +++ b/pkg/exec/run.go @@ -21,25 +21,32 @@ import ( "os/signal" "path/filepath" "strings" + "sync" "syscall" "time" - "github.com/fatih/color" "github.com/pingcap/errors" "github.com/pingcap/tiup/pkg/environment" "github.com/pingcap/tiup/pkg/localdata" "github.com/pingcap/tiup/pkg/telemetry" + "github.com/pingcap/tiup/pkg/tui/colorstr" "github.com/pingcap/tiup/pkg/utils" "github.com/pingcap/tiup/pkg/version" "golang.org/x/mod/semver" ) +// Skip displaying "Starting component ..." message for some commonly used components. +var skipStartingMessages = map[string]bool{ + "playground": true, + "cluster": true, +} + // RunComponent start a component and wait it func RunComponent(env *environment.Environment, tag, spec, binPath string, args []string) error { component, version := environment.ParseCompVersion(spec) if version == "" { - cmdCheckUpdate(component, version, 2) + cmdCheckUpdate(component, version) } binPath, err := PrepareBinary(component, version, binPath) @@ -74,7 +81,10 @@ func RunComponent(env *environment.Environment, tag, spec, binPath string, args return err } - fmt.Fprintf(os.Stderr, "Starting component `%s`: %s\n", component, strings.Join(c.Args, " ")) + if skip, ok := skipStartingMessages[component]; !skip || !ok { + colorstr.Fprintf(os.Stderr, "Starting component [bold]%s[reset]: %s\n", component, strings.Join(c.Args, " ")) + } + err = c.Start() if err != nil { return errors.Annotatef(err, "Failed to start component `%s`", component) @@ -170,37 +180,58 @@ func PrepareCommand(p *PrepareCommandParams) (*exec.Cmd, error) { return c, nil } -func cmdCheckUpdate(component string, version utils.Version, timeoutSec int) { - fmt.Fprintf(os.Stderr, "tiup is checking updates for component %s ...", component) - updateC := make(chan string) - // timeout for check update +func cmdCheckUpdate(component string, version utils.Version) { + const ( + slowTimeout = 1 * time.Second // Timeout to display checking message + cancelTimeout = 2 * time.Second // Timeout to cancel the check + ) + + // This mutex is used for protecting flag as well as stdout + mu := sync.Mutex{} + isCheckFinished := false + + result := make(chan string, 1) + go func() { - time.Sleep(time.Duration(timeoutSec) * time.Second) - updateC <- color.YellowString("timeout(%ds)!", timeoutSec) + time.Sleep(slowTimeout) + mu.Lock() + defer mu.Unlock() + if !isCheckFinished { + colorstr.Fprintf(os.Stderr, "Checking updates for component [bold]%s[reset]... ", component) + } + }() + + go func() { + time.Sleep(cancelTimeout) + result <- colorstr.Sprintf("[yellow]Timedout (after %s)", cancelTimeout) }() go func() { - var updateInfo string latestV, _, err := environment.GlobalEnv().V1Repository().LatestStableVersion(component, false) if err != nil { + result <- "" return } selectVer, _ := environment.GlobalEnv().SelectInstalledVersion(component, version) if semver.Compare(selectVer.String(), latestV.String()) < 0 { - updateInfo = fmt.Sprint(color.YellowString(` -A new version of %[1]s is available: - The latest version: %[2]s - Local installed version: %[3]s - Update current component: tiup update %[1]s - Update all components: tiup update --all + result <- colorstr.Sprintf(` +[yellow]A new version of [bold]%[1]s[reset][yellow] is available:[reset] [red][bold]%[2]s[reset] -> [green][bold]%[3]s[reset] + + To update this component: [tiup_command]tiup update %[1]s[reset] + To update all components: [tiup_command]tiup update --all[reset] `, - component, latestV.String(), selectVer.String())) + component, selectVer.String(), latestV.String()) } - updateC <- updateInfo }() - fmt.Fprintln(os.Stderr, <-updateC) + s := <-result + mu.Lock() + defer mu.Unlock() + isCheckFinished = true + if len(s) > 0 { + fmt.Fprintln(os.Stderr, s) + } } // PrepareBinary use given binpath or download from tiup mirror diff --git a/pkg/tui/colorstr/builtin_tokens.go b/pkg/tui/colorstr/builtin_tokens.go new file mode 100644 index 0000000000..6e67d68f13 --- /dev/null +++ b/pkg/tui/colorstr/builtin_tokens.go @@ -0,0 +1,28 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package colorstr + +import ( + "fmt" + + "github.com/mitchellh/colorstring" +) + +func init() { + // Register tiup specific color tokens + DefaultTokens.Colors["tiup_command"] = fmt.Sprintf("%s;%s", + colorstring.DefaultColors["light_cyan"], + colorstring.DefaultColors["bold"], + ) +} diff --git a/pkg/tui/colorstr/color.go b/pkg/tui/colorstr/color.go new file mode 100644 index 0000000000..98f19cf2c1 --- /dev/null +++ b/pkg/tui/colorstr/color.go @@ -0,0 +1,91 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package colorstr interprets the input format containing color tokens like `[blue]hello [red]world` +// as the text "hello world" in two colors. +// +// Just like tokens in the fmt package (e.g. '%s'), color tokens will only be effective when specified +// as the format parameter. Tokens not in the format parameter will not be interpreted. +// +// colorstr.DefaultTokens.Printf("[blue]hello") ==> (a blue hello) +// colorstr.DefaultTokens.Printf("[ahh]") ==> "[ahh]" +// +// Color tokens in the Print arguments will never be interpreted. It can be useful to pass user inputs there. +package colorstr + +import ( + "fmt" + "io" + + "github.com/mitchellh/colorstring" +) + +type colorTokens struct { + colorstring.Colorize +} + +// Note: Print, Println, Fprint, Fprintln are intentionally not implemented, as we would like to +// limit the usage of color token to be only placed in the "format" part. + +// Printf is a convenience wrapper for fmt.Printf with support for color codes. +// Only color codes in the format param will be respected. +func (c colorTokens) Printf(format string, a ...any) (n int, err error) { + return fmt.Printf(c.Color(format), a...) +} + +// Fprintf is a convenience wrapper for fmt.Fprintf with support for color codes. +// Only color codes in the format param will be respected. +func (c colorTokens) Fprintf(w io.Writer, format string, a ...any) (n int, err error) { + return fmt.Fprintf(w, c.Color(format), a...) +} + +// Sprintf is a convenience wrapper for fmt.Sprintf with support for color codes. +// Only color codes in the format param will be respected. +func (c colorTokens) Sprintf(format string, a ...any) string { + return fmt.Sprintf(c.Color(format), a...) +} + +// DefaultTokens uses default color tokens. +var DefaultTokens = (func() colorTokens { + // TODO: Respect NO_COLOR env + // TODO: Add more color tokens here + colors := make(map[string]string) + for k, v := range colorstring.DefaultColors { + colors[k] = v + } + return colorTokens{ + Colorize: colorstring.Colorize{ + Colors: colors, + Disable: false, + Reset: true, + }, + } +})() + +// Printf is a convenience wrapper for fmt.Printf with support for color codes. +// Only color codes in the format param will be respected. +func Printf(format string, a ...any) (n int, err error) { + return DefaultTokens.Printf(format, a...) +} + +// Fprintf is a convenience wrapper for fmt.Fprintf with support for color codes. +// Only color codes in the format param will be respected. +func Fprintf(w io.Writer, format string, a ...any) (n int, err error) { + return DefaultTokens.Fprintf(w, format, a...) +} + +// Sprintf is a convenience wrapper for fmt.Sprintf with support for color codes. +// Only color codes in the format param will be respected. +func Sprintf(format string, a ...any) string { + return DefaultTokens.Sprintf(format, a...) +} diff --git a/pkg/tui/colorstr/color_test.go b/pkg/tui/colorstr/color_test.go new file mode 100644 index 0000000000..e4bf5d8096 --- /dev/null +++ b/pkg/tui/colorstr/color_test.go @@ -0,0 +1,31 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package colorstr + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDefaultTokens(t *testing.T) { + require.Equal(t, "[blue", DefaultTokens.Sprintf("[blue")) + require.Equal(t, "hello", DefaultTokens.Sprintf("hello")) + require.Equal(t, "\x1B[34mhello\x1B[0m", DefaultTokens.Sprintf("[blue]hello")) + require.Equal(t, "\x1B[34mhello\x1B[0m\x1B[0m", DefaultTokens.Sprintf("[blue]hello[reset]")) + require.Equal(t, "\x1B[34mhello\x1B[0mfoo\x1B[0m", DefaultTokens.Sprintf("[blue]hello[reset]foo")) + require.Equal(t, "\x1B[34mhello\x1B[31mfoo\x1B[0m", DefaultTokens.Sprintf("[blue]hello[red]foo")) + require.Equal(t, "\x1B[34mhello [blue]\x1B[0m", DefaultTokens.Sprintf("[blue]hello %s", "[blue]")) + require.Equal(t, "[ahh]hello", DefaultTokens.Sprintf("[ahh]hello")) +} diff --git a/pkg/tui/colorstr/test_util.go b/pkg/tui/colorstr/test_util.go new file mode 100644 index 0000000000..f6d6f4f4bb --- /dev/null +++ b/pkg/tui/colorstr/test_util.go @@ -0,0 +1,30 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package colorstr + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// RequireEqualColorToken compares whether the actual string is equal to the expected string after color processing. +func RequireEqualColorToken(t *testing.T, expectColorTokens string, actualString string) { + require.Equal(t, DefaultTokens.Color(expectColorTokens), actualString) +} + +// RequireNotEqualColorToken compares whether the actual string is not equal to the expected string after color processing. +func RequireNotEqualColorToken(t *testing.T, expectColorTokens string, actualString string) { + require.NotEqual(t, DefaultTokens.Color(expectColorTokens), actualString) +} diff --git a/pkg/tui/colorstr/test_util_test.go b/pkg/tui/colorstr/test_util_test.go new file mode 100644 index 0000000000..49218f6585 --- /dev/null +++ b/pkg/tui/colorstr/test_util_test.go @@ -0,0 +1,25 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package colorstr + +import ( + "testing" +) + +func TestRequireEqualColorToken(t *testing.T) { + RequireEqualColorToken(t, "[red]hello", DefaultTokens.Color("[red]hello")) + RequireNotEqualColorToken(t, "[yellow]hello", DefaultTokens.Color("[red]hello")) + RequireEqualColorToken(t, "[red]hello[reset]", DefaultTokens.Color("[red]hello[reset]")) + RequireNotEqualColorToken(t, "[red]hello", DefaultTokens.Color("[red]hello[reset]")) +}