From d68c6c901df2bb9ede8256d9916e871504b7ee59 Mon Sep 17 00:00:00 2001 From: Alexander Arvidsson Date: Sun, 31 Mar 2024 17:19:44 +0200 Subject: [PATCH 1/6] feat: Colorize tasks in prefixed output --- internal/flags/flags.go | 7 +++ internal/output/color.go | 97 ++++++++++++++++++++++++++++++++++ internal/output/output.go | 4 +- internal/output/output_test.go | 31 ++++++++++- internal/output/prefixed.go | 60 ++++++++++++++++++--- setup.go | 2 +- taskfile/ast/output.go | 37 ++++++++++--- website/docs/api_reference.mdx | 1 + website/docs/usage.mdx | 16 ++++++ website/static/schema.json | 30 +++++++---- 10 files changed, 257 insertions(+), 28 deletions(-) create mode 100644 internal/output/color.go diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 6c67443c5..339a5888c 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -100,6 +100,7 @@ func init() { pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.") pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.") pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.") + pflag.BoolVar(&Output.Prefix.Color, "output-prefix-color", false, "Use colors for task prefixes.") pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number tasks to run concurrently.") pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.") @@ -146,5 +147,11 @@ func Validate() error { } } + if Output.Name != "prefixed" { + if Output.Prefix.Color { + return errors.New("task: You can't set --output-prefix-color without --output=prefixed") + } + } + return nil } diff --git a/internal/output/color.go b/internal/output/color.go new file mode 100644 index 000000000..e430eb073 --- /dev/null +++ b/internal/output/color.go @@ -0,0 +1,97 @@ +/* +Source code in this file is based on the code from the Go-Chi project. +https://github.com/go-chi/chi/blob/master/middleware/terminal.go + +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package output + +import ( + "fmt" + "io" + "os" +) + +var ( + // Normal colors + // nBlack = []byte{'\033', '[', '3', '0', 'm'} UNUSED + nRed = []byte{'\033', '[', '3', '1', 'm'} + nGreen = []byte{'\033', '[', '3', '2', 'm'} + nYellow = []byte{'\033', '[', '3', '3', 'm'} + nBlue = []byte{'\033', '[', '3', '4', 'm'} + nMagenta = []byte{'\033', '[', '3', '5', 'm'} + nCyan = []byte{'\033', '[', '3', '6', 'm'} + // nWhite = []byte{'\033', '[', '3', '7', 'm'} UNUSED + // Bright colors + // bBlack = []byte{'\033', '[', '3', '0', ';', '1', 'm'} UNUSED + bRed = []byte{'\033', '[', '3', '1', ';', '1', 'm'} + bGreen = []byte{'\033', '[', '3', '2', ';', '1', 'm'} + bYellow = []byte{'\033', '[', '3', '3', ';', '1', 'm'} + bBlue = []byte{'\033', '[', '3', '4', ';', '1', 'm'} + bMagenta = []byte{'\033', '[', '3', '5', ';', '1', 'm'} + bCyan = []byte{'\033', '[', '3', '6', ';', '1', 'm'} + // bWhite = []byte{'\033', '[', '3', '7', ';', '1', 'm'} UNUSED + + reset = []byte{'\033', '[', '0', 'm'} +) + +// This is public so we can override it in tests. +var IsTTY bool + +func init() { + // This is sort of cheating: if stdout is a character device, we assume + // that means it's a TTY. Unfortunately, there are many non-TTY + // character devices, but fortunately stdout is rarely set to any of + // them. + // + // We could solve this properly by pulling in a dependency on + // code.google.com/p/go.crypto/ssh/terminal, for instance, but as a + // heuristic for whether to print in color or in black-and-white, I'd + // really rather not. + fi, err := os.Stdout.Stat() + if err == nil { + m := os.ModeDevice | os.ModeCharDevice + IsTTY = fi.Mode()&m == m + } +} + +// colorWrite +func cW(w io.Writer, useColor bool, color []byte, s string, args ...any) error { + if IsTTY && useColor { + if _, err := w.Write(color); err != nil { + return err + } + } + + if _, err := fmt.Fprintf(w, s, args...); err != nil { + return err + } + + if IsTTY && useColor { + if _, err := w.Write(reset); err != nil { + return err + } + } + + return nil +} diff --git a/internal/output/output.go b/internal/output/output.go index c3c1346c3..dba294a40 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -15,7 +15,7 @@ type Output interface { type CloseFunc func(err error) error // Build the Output for the requested ast.Output. -func BuildFor(o *ast.Output) (Output, error) { +func BuildFor(o *ast.Output, color bool) (Output, error) { switch o.Name { case "interleaved", "": if err := checkOutputGroupUnset(o); err != nil { @@ -32,7 +32,7 @@ func BuildFor(o *ast.Output) (Output, error) { if err := checkOutputGroupUnset(o); err != nil { return nil, err } - return Prefixed{}, nil + return NewPrefixed(color && o.Prefix.Color), nil default: return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name) } diff --git a/internal/output/output_test.go b/internal/output/output_test.go index 41b35552a..1e6230d71 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -107,7 +107,7 @@ func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) { func TestPrefixed(t *testing.T) { var b bytes.Buffer - var o output.Output = output.Prefixed{} + var o output.Output = output.NewPrefixed(false) w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil) t.Run("simple use cases", func(t *testing.T) { @@ -132,3 +132,32 @@ func TestPrefixed(t *testing.T) { assert.Equal(t, "[prefix] Test!\n", b.String()) }) } + +func wrapColor(string string, color string) string { + return fmt.Sprintf("%s%s\033[0m", color, string) +} + +func TestPrefixedWithColor(t *testing.T) { + // We must set IsTTY to include color codes in the output + output.IsTTY = true + + var b bytes.Buffer + var o output.Output = output.NewPrefixed(true) + + writers := make([]io.Writer, 16) + for i := range writers { + writers[i], _, _ = o.WrapWriter(&b, io.Discard, fmt.Sprintf("prefix-%d", i), nil) + } + + t.Run("colors should loop", func(t *testing.T) { + for i, w := range writers { + b.Reset() + + color := output.PrefixColorSequence[i%len(output.PrefixColorSequence)] + prefix := wrapColor(fmt.Sprintf("prefix-%d", i), string(color)) + + fmt.Fprintln(w, "foo\nbar") + assert.Equal(t, fmt.Sprintf("[%s] foo\n[%s] bar\n", prefix, prefix), b.String()) + } + }) +} diff --git a/internal/output/prefixed.go b/internal/output/prefixed.go index cea2c7d35..2aa0aa5af 100644 --- a/internal/output/prefixed.go +++ b/internal/output/prefixed.go @@ -9,17 +9,34 @@ import ( "github.com/go-task/task/v3/internal/templater" ) -type Prefixed struct{} +type Prefixed struct { + seen map[string]uint + counter *uint + Color bool +} + +func NewPrefixed(color bool) Prefixed { + var counter uint + + return Prefixed{ + Color: color, + counter: &counter, + seen: make(map[string]uint), + } +} -func (Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) { - pw := &prefixWriter{writer: stdOut, prefix: prefix} +func (p Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) { + pw := &prefixWriter{writer: stdOut, prefix: prefix, color: p.Color, seen: p.seen, counter: p.counter} return pw, pw, func(error) error { return pw.close() } } type prefixWriter struct { - writer io.Writer - prefix string - buff bytes.Buffer + writer io.Writer + seen map[string]uint + counter *uint + prefix string + buff bytes.Buffer + color bool } func (pw *prefixWriter) Write(p []byte) (int, error) { @@ -56,6 +73,11 @@ func (pw *prefixWriter) writeOutputLines(force bool) error { } } +var PrefixColorSequence = [][]byte{ + nYellow, nBlue, nMagenta, nCyan, nGreen, nRed, + bYellow, bBlue, bMagenta, bCyan, bGreen, bRed, +} + func (pw *prefixWriter) writeLine(line string) error { if line == "" { return nil @@ -63,6 +85,30 @@ func (pw *prefixWriter) writeLine(line string) error { if !strings.HasSuffix(line, "\n") { line += "\n" } - _, err := fmt.Fprintf(pw.writer, "[%s] %s", pw.prefix, line) + + idx, ok := pw.seen[pw.prefix] + + if !ok { + idx = *pw.counter + pw.seen[pw.prefix] = idx + + *pw.counter += 1 + } + + color := PrefixColorSequence[idx%uint(len(PrefixColorSequence))] + + if _, err := fmt.Fprint(pw.writer, "["); err != nil { + return nil + } + + if err := cW(pw.writer, pw.color, color, "%s", pw.prefix); err != nil { + return err + } + + if _, err := fmt.Fprint(pw.writer, "] "); err != nil { + return nil + } + + _, err := fmt.Fprint(pw.writer, line) return err } diff --git a/setup.go b/setup.go index 1387f5290..815d47842 100644 --- a/setup.go +++ b/setup.go @@ -155,7 +155,7 @@ func (e *Executor) setupOutput() error { } var err error - e.Output, err = output.BuildFor(&e.OutputStyle) + e.Output, err = output.BuildFor(&e.OutputStyle, e.Color) return err } diff --git a/taskfile/ast/output.go b/taskfile/ast/output.go index 79c4c113f..6a9aa017e 100644 --- a/taskfile/ast/output.go +++ b/taskfile/ast/output.go @@ -12,6 +12,8 @@ type Output struct { Name string `yaml:"-"` // Group specific style Group OutputGroup + // Prefix specific style + Prefix OutputPrefix } // IsSet returns true if and only if a custom output style is set. @@ -32,19 +34,33 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error { case yaml.MappingNode: var tmp struct { - Group *OutputGroup + Group *OutputGroup + Prefixed *OutputPrefix } + if err := node.Decode(&tmp); err != nil { - return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err) + return fmt.Errorf("task: output style must be a string or mapping with a \"group\" or \"prefixed\" key: %w", err) } - if tmp.Group == nil { - return fmt.Errorf("task: output style must have the \"group\" key when in mapping form") + + if tmp.Group != nil { + *s = Output{ + Name: "group", + Group: *tmp.Group, + } + + return nil } - *s = Output{ - Name: "group", - Group: *tmp.Group, + + if tmp.Prefixed != nil { + *s = Output{ + Name: "prefixed", + Prefix: *tmp.Prefixed, + } + + return nil } - return nil + + return fmt.Errorf("task: output style must have either \"group\" or \"prefixed\" key when in mapping form") } return fmt.Errorf("yaml: line %d: cannot unmarshal %s into output", node.Line, node.ShortTag()) @@ -63,3 +79,8 @@ func (g *OutputGroup) IsSet() bool { } return g.Begin != "" || g.End != "" } + +// OutputGroup is the style options specific to the Group style. +type OutputPrefix struct { + Color bool `yaml:"color"` +} diff --git a/website/docs/api_reference.mdx b/website/docs/api_reference.mdx index 43f7ff49c..1c87ab8ba 100644 --- a/website/docs/api_reference.mdx +++ b/website/docs/api_reference.mdx @@ -42,6 +42,7 @@ If `--` is given, all remaining arguments will be assigned to a special | | `--output-group-begin` | `string` | | Message template to print before a task's grouped output. | | | `--output-group-end` | `string` | | Message template to print after a task's grouped output. | | | `--output-group-error-only` | `bool` | `false` | Swallow command output on zero exit code. | +| | `--output-prefix-color ` | `bool` | `false` | Use colors for task prefixes. | | `-p` | `--parallel` | `bool` | `false` | Executes tasks provided on command line in parallel. | | `-s` | `--silent` | `bool` | `false` | Disables echoing. | | `-y` | `--yes` | `bool` | `false` | Assume "yes" as answer to all prompts. | diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx index 50a010e16..0462bc8a4 100644 --- a/website/docs/usage.mdx +++ b/website/docs/usage.mdx @@ -1885,6 +1885,22 @@ $ task default [print-baz] baz ``` +When using the `prefix` output, you can optionally configure it to colorize each prefix. +The chosen color is deterministic for a given set of tasks, where the color will cycle +through a predefined sequence of colors. If more tasks are run than colors available, the +color will cycle back and repeat. This can be useful for visually distinguishing tasks in +the output when using the `prefix` output. +```yaml +version: '3' + +output: + prefixed: + color: true + +tasks: + # ... +``` + :::tip The `output` option can also be specified by the `--output` or `-o` flags. diff --git a/website/static/schema.json b/website/static/schema.json index fb5c3364e..9c4e00712 100644 --- a/website/static/schema.json +++ b/website/static/schema.json @@ -254,7 +254,15 @@ "^.*$": { "anyOf": [ { - "type": ["boolean", "integer", "null", "number", "string", "object", "array"] + "type": [ + "boolean", + "integer", + "null", + "number", + "string", + "object", + "array" + ] }, { "$ref": "#/definitions/var_subkey" @@ -396,10 +404,7 @@ "$ref": "#/definitions/vars" } }, - "oneOf": [ - {"required": ["cmd"]}, - {"required": ["task"]} - ], + "oneOf": [{ "required": ["cmd"] }, { "required": ["task"] }], "additionalProperties": false, "required": ["for"] }, @@ -422,10 +427,7 @@ "$ref": "#/definitions/vars" } }, - "oneOf": [ - {"required": ["cmd"]}, - {"required": ["task"]} - ], + "oneOf": [{ "required": ["cmd"] }, { "required": ["task"] }], "additionalProperties": false, "required": ["for"] }, @@ -544,6 +546,16 @@ "default": false } } + }, + "prefixed": { + "type": "object", + "properties": { + "color": { + "description": "Use colors for task prefixes.", + "type": "boolean", + "default": false + } + } } } }, From 2aab5a11ddb477d591767f8296960c897d1f67bc Mon Sep 17 00:00:00 2001 From: Alexander Arvidsson Date: Sun, 31 Mar 2024 17:26:53 +0200 Subject: [PATCH 2/6] chore: comment and style changes --- taskfile/ast/output.go | 2 +- website/static/schema.json | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/taskfile/ast/output.go b/taskfile/ast/output.go index 6a9aa017e..d3fd0294e 100644 --- a/taskfile/ast/output.go +++ b/taskfile/ast/output.go @@ -80,7 +80,7 @@ func (g *OutputGroup) IsSet() bool { return g.Begin != "" || g.End != "" } -// OutputGroup is the style options specific to the Group style. +// OutputPrefix is the style options specific to the Prefix style. type OutputPrefix struct { Color bool `yaml:"color"` } diff --git a/website/static/schema.json b/website/static/schema.json index 9c4e00712..c820f0d16 100644 --- a/website/static/schema.json +++ b/website/static/schema.json @@ -254,15 +254,7 @@ "^.*$": { "anyOf": [ { - "type": [ - "boolean", - "integer", - "null", - "number", - "string", - "object", - "array" - ] + "type": ["boolean", "integer", "null", "number", "string", "object", "array"] }, { "$ref": "#/definitions/var_subkey" @@ -404,7 +396,10 @@ "$ref": "#/definitions/vars" } }, - "oneOf": [{ "required": ["cmd"] }, { "required": ["task"] }], + "oneOf": [ + {"required": ["cmd"]}, + {"required": ["task"]} + ], "additionalProperties": false, "required": ["for"] }, @@ -427,7 +422,10 @@ "$ref": "#/definitions/vars" } }, - "oneOf": [{ "required": ["cmd"] }, { "required": ["task"] }], + "oneOf": [ + {"required": ["cmd"]}, + {"required": ["task"]} + ], "additionalProperties": false, "required": ["for"] }, From 6f9ba53b1030ac3b3decc5afc1f82cf4bd3de7b4 Mon Sep 17 00:00:00 2001 From: Alexander Arvidsson Date: Sun, 31 Mar 2024 17:49:05 +0200 Subject: [PATCH 3/6] fix code tag has spaces in api reference --- website/docs/api_reference.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/api_reference.mdx b/website/docs/api_reference.mdx index 1c87ab8ba..7443dbdf2 100644 --- a/website/docs/api_reference.mdx +++ b/website/docs/api_reference.mdx @@ -42,7 +42,7 @@ If `--` is given, all remaining arguments will be assigned to a special | | `--output-group-begin` | `string` | | Message template to print before a task's grouped output. | | | `--output-group-end` | `string` | | Message template to print after a task's grouped output. | | | `--output-group-error-only` | `bool` | `false` | Swallow command output on zero exit code. | -| | `--output-prefix-color ` | `bool` | `false` | Use colors for task prefixes. | +| | `--output-prefix-color` | `bool` | `false` | Use colors for task prefixes. | | `-p` | `--parallel` | `bool` | `false` | Executes tasks provided on command line in parallel. | | `-s` | `--silent` | `bool` | `false` | Disables echoing. | | `-y` | `--yes` | `bool` | `false` | Assume "yes" as answer to all prompts. | From a63ab36339bbe2749d2cb6191dc9a21aa971ad19 Mon Sep 17 00:00:00 2001 From: Alexander Arvidsson Date: Mon, 1 Apr 2024 12:17:06 +0200 Subject: [PATCH 4/6] fix: migrate to use logger for colors --- internal/output/color.go | 97 ---------------------------------- internal/output/output.go | 5 +- internal/output/output_test.go | 27 ++++++---- internal/output/prefixed.go | 48 +++++++++-------- setup.go | 2 +- 5 files changed, 47 insertions(+), 132 deletions(-) delete mode 100644 internal/output/color.go diff --git a/internal/output/color.go b/internal/output/color.go deleted file mode 100644 index e430eb073..000000000 --- a/internal/output/color.go +++ /dev/null @@ -1,97 +0,0 @@ -/* -Source code in this file is based on the code from the Go-Chi project. -https://github.com/go-chi/chi/blob/master/middleware/terminal.go - -Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -package output - -import ( - "fmt" - "io" - "os" -) - -var ( - // Normal colors - // nBlack = []byte{'\033', '[', '3', '0', 'm'} UNUSED - nRed = []byte{'\033', '[', '3', '1', 'm'} - nGreen = []byte{'\033', '[', '3', '2', 'm'} - nYellow = []byte{'\033', '[', '3', '3', 'm'} - nBlue = []byte{'\033', '[', '3', '4', 'm'} - nMagenta = []byte{'\033', '[', '3', '5', 'm'} - nCyan = []byte{'\033', '[', '3', '6', 'm'} - // nWhite = []byte{'\033', '[', '3', '7', 'm'} UNUSED - // Bright colors - // bBlack = []byte{'\033', '[', '3', '0', ';', '1', 'm'} UNUSED - bRed = []byte{'\033', '[', '3', '1', ';', '1', 'm'} - bGreen = []byte{'\033', '[', '3', '2', ';', '1', 'm'} - bYellow = []byte{'\033', '[', '3', '3', ';', '1', 'm'} - bBlue = []byte{'\033', '[', '3', '4', ';', '1', 'm'} - bMagenta = []byte{'\033', '[', '3', '5', ';', '1', 'm'} - bCyan = []byte{'\033', '[', '3', '6', ';', '1', 'm'} - // bWhite = []byte{'\033', '[', '3', '7', ';', '1', 'm'} UNUSED - - reset = []byte{'\033', '[', '0', 'm'} -) - -// This is public so we can override it in tests. -var IsTTY bool - -func init() { - // This is sort of cheating: if stdout is a character device, we assume - // that means it's a TTY. Unfortunately, there are many non-TTY - // character devices, but fortunately stdout is rarely set to any of - // them. - // - // We could solve this properly by pulling in a dependency on - // code.google.com/p/go.crypto/ssh/terminal, for instance, but as a - // heuristic for whether to print in color or in black-and-white, I'd - // really rather not. - fi, err := os.Stdout.Stat() - if err == nil { - m := os.ModeDevice | os.ModeCharDevice - IsTTY = fi.Mode()&m == m - } -} - -// colorWrite -func cW(w io.Writer, useColor bool, color []byte, s string, args ...any) error { - if IsTTY && useColor { - if _, err := w.Write(color); err != nil { - return err - } - } - - if _, err := fmt.Fprintf(w, s, args...); err != nil { - return err - } - - if IsTTY && useColor { - if _, err := w.Write(reset); err != nil { - return err - } - } - - return nil -} diff --git a/internal/output/output.go b/internal/output/output.go index dba294a40..df7db9a71 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -4,6 +4,7 @@ import ( "fmt" "io" + "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) @@ -15,7 +16,7 @@ type Output interface { type CloseFunc func(err error) error // Build the Output for the requested ast.Output. -func BuildFor(o *ast.Output, color bool) (Output, error) { +func BuildFor(o *ast.Output, logger *logger.Logger) (Output, error) { switch o.Name { case "interleaved", "": if err := checkOutputGroupUnset(o); err != nil { @@ -32,7 +33,7 @@ func BuildFor(o *ast.Output, color bool) (Output, error) { if err := checkOutputGroupUnset(o); err != nil { return nil, err } - return NewPrefixed(color && o.Prefix.Color), nil + return NewPrefixed(logger, o.Prefix.Color), nil default: return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name) } diff --git a/internal/output/output_test.go b/internal/output/output_test.go index 1e6230d71..99fc3cc22 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -7,9 +7,11 @@ import ( "io" "testing" + "github.com/fatih/color" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/templater" @@ -107,7 +109,11 @@ func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) { func TestPrefixed(t *testing.T) { var b bytes.Buffer - var o output.Output = output.NewPrefixed(false) + l := &logger.Logger{ + Color: false, + } + + var o output.Output = output.NewPrefixed(l, false) w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil) t.Run("simple use cases", func(t *testing.T) { @@ -133,16 +139,15 @@ func TestPrefixed(t *testing.T) { }) } -func wrapColor(string string, color string) string { - return fmt.Sprintf("%s%s\033[0m", color, string) -} - func TestPrefixedWithColor(t *testing.T) { - // We must set IsTTY to include color codes in the output - output.IsTTY = true + color.NoColor = false var b bytes.Buffer - var o output.Output = output.NewPrefixed(true) + l := &logger.Logger{ + Color: true, + } + + var o output.Output = output.NewPrefixed(l, true) writers := make([]io.Writer, 16) for i := range writers { @@ -154,10 +159,12 @@ func TestPrefixedWithColor(t *testing.T) { b.Reset() color := output.PrefixColorSequence[i%len(output.PrefixColorSequence)] - prefix := wrapColor(fmt.Sprintf("prefix-%d", i), string(color)) + + var prefix bytes.Buffer + l.FOutf(&prefix, color, fmt.Sprintf("prefix-%d", i)) fmt.Fprintln(w, "foo\nbar") - assert.Equal(t, fmt.Sprintf("[%s] foo\n[%s] bar\n", prefix, prefix), b.String()) + assert.Equal(t, fmt.Sprintf("[%s] foo\n[%s] bar\n", prefix.String(), prefix.String()), b.String()) } }) } diff --git a/internal/output/prefixed.go b/internal/output/prefixed.go index 2aa0aa5af..9587208ca 100644 --- a/internal/output/prefixed.go +++ b/internal/output/prefixed.go @@ -6,37 +6,38 @@ import ( "io" "strings" + "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/templater" ) type Prefixed struct { + logger *logger.Logger seen map[string]uint counter *uint - Color bool + color bool } -func NewPrefixed(color bool) Prefixed { +func NewPrefixed(logger *logger.Logger, color bool) Prefixed { var counter uint return Prefixed{ - Color: color, - counter: &counter, seen: make(map[string]uint), + counter: &counter, + logger: logger, + color: color, } } func (p Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) { - pw := &prefixWriter{writer: stdOut, prefix: prefix, color: p.Color, seen: p.seen, counter: p.counter} + pw := &prefixWriter{writer: stdOut, prefix: prefix, prefixed: &p} return pw, pw, func(error) error { return pw.close() } } type prefixWriter struct { - writer io.Writer - seen map[string]uint - counter *uint - prefix string - buff bytes.Buffer - color bool + writer io.Writer + prefixed *Prefixed + prefix string + buff bytes.Buffer } func (pw *prefixWriter) Write(p []byte) (int, error) { @@ -73,9 +74,9 @@ func (pw *prefixWriter) writeOutputLines(force bool) error { } } -var PrefixColorSequence = [][]byte{ - nYellow, nBlue, nMagenta, nCyan, nGreen, nRed, - bYellow, bBlue, bMagenta, bCyan, bGreen, bRed, +var PrefixColorSequence = []logger.Color{ + logger.Yellow, logger.Blue, logger.Magenta, logger.Cyan, logger.Green, logger.Red, + // bYellow, bBlue, bMagenta, bCyan, bGreen, bRed, } func (pw *prefixWriter) writeLine(line string) error { @@ -86,23 +87,26 @@ func (pw *prefixWriter) writeLine(line string) error { line += "\n" } - idx, ok := pw.seen[pw.prefix] + idx, ok := pw.prefixed.seen[pw.prefix] if !ok { - idx = *pw.counter - pw.seen[pw.prefix] = idx + idx = *pw.prefixed.counter + pw.prefixed.seen[pw.prefix] = idx - *pw.counter += 1 + *pw.prefixed.counter++ } - color := PrefixColorSequence[idx%uint(len(PrefixColorSequence))] - if _, err := fmt.Fprint(pw.writer, "["); err != nil { return nil } - if err := cW(pw.writer, pw.color, color, "%s", pw.prefix); err != nil { - return err + if pw.prefixed.color { + color := PrefixColorSequence[idx%uint(len(PrefixColorSequence))] + pw.prefixed.logger.FOutf(pw.writer, color, pw.prefix) + } else { + if _, err := fmt.Fprint(pw.writer, pw.prefix); err != nil { + return nil + } } if _, err := fmt.Fprint(pw.writer, "] "); err != nil { diff --git a/setup.go b/setup.go index 815d47842..a877a5971 100644 --- a/setup.go +++ b/setup.go @@ -155,7 +155,7 @@ func (e *Executor) setupOutput() error { } var err error - e.Output, err = output.BuildFor(&e.OutputStyle, e.Color) + e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger) return err } From d2ff59c063b7549a5ec20657c4617eb5e68f44f3 Mon Sep 17 00:00:00 2001 From: Alexander Arvidsson Date: Mon, 1 Apr 2024 12:44:08 +0200 Subject: [PATCH 5/6] fix: Add bright colors to the color sequence --- internal/logger/logger.go | 24 ++++++++++++++++++++++++ internal/output/prefixed.go | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 8d72539be..61f15b84c 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -52,6 +52,30 @@ func Red() PrintFunc { return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc() } +func BrightBlue() PrintFunc { + return color.New(envColor("TASK_COLOR_BRIGHT_BLUE", color.FgHiBlue)...).FprintfFunc() +} + +func BrightGreen() PrintFunc { + return color.New(envColor("TASK_COLOR_BRIGHT_GREEN", color.FgHiGreen)...).FprintfFunc() +} + +func BrightCyan() PrintFunc { + return color.New(envColor("TASK_COLOR_BRIGHT_CYAN", color.FgHiCyan)...).FprintfFunc() +} + +func BrightYellow() PrintFunc { + return color.New(envColor("TASK_COLOR_BRIGHT_YELLOW", color.FgHiYellow)...).FprintfFunc() +} + +func BrightMagenta() PrintFunc { + return color.New(envColor("TASK_COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)...).FprintfFunc() +} + +func BrightRed() PrintFunc { + return color.New(envColor("TASK_COLOR_BRIGHT_RED", color.FgHiRed)...).FprintfFunc() +} + func envColor(env string, defaultColor color.Attribute) []color.Attribute { if os.Getenv("FORCE_COLOR") != "" { color.NoColor = false diff --git a/internal/output/prefixed.go b/internal/output/prefixed.go index 9587208ca..58386f9e1 100644 --- a/internal/output/prefixed.go +++ b/internal/output/prefixed.go @@ -76,7 +76,7 @@ func (pw *prefixWriter) writeOutputLines(force bool) error { var PrefixColorSequence = []logger.Color{ logger.Yellow, logger.Blue, logger.Magenta, logger.Cyan, logger.Green, logger.Red, - // bYellow, bBlue, bMagenta, bCyan, bGreen, bRed, + logger.BrightYellow, logger.BrightBlue, logger.BrightMagenta, logger.BrightCyan, logger.BrightGreen, logger.BrightRed, } func (pw *prefixWriter) writeLine(line string) error { From 035dfd744762a6fdfba98dbc45a40ccf7da53a9e Mon Sep 17 00:00:00 2001 From: Alexander Arvidsson Date: Mon, 29 Apr 2024 18:31:26 +0200 Subject: [PATCH 6/6] fix: make colorized prefix logger standard --- internal/flags/flags.go | 7 ------- internal/output/output.go | 2 +- internal/output/output_test.go | 4 ++-- internal/output/prefixed.go | 14 +++---------- taskfile/ast/output.go | 37 ++++++++-------------------------- website/docs/api_reference.mdx | 1 - website/docs/usage.mdx | 16 --------------- website/static/schema.json | 10 --------- 8 files changed, 14 insertions(+), 77 deletions(-) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 339a5888c..6c67443c5 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -100,7 +100,6 @@ func init() { pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.") pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.") pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.") - pflag.BoolVar(&Output.Prefix.Color, "output-prefix-color", false, "Use colors for task prefixes.") pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number tasks to run concurrently.") pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.") @@ -147,11 +146,5 @@ func Validate() error { } } - if Output.Name != "prefixed" { - if Output.Prefix.Color { - return errors.New("task: You can't set --output-prefix-color without --output=prefixed") - } - } - return nil } diff --git a/internal/output/output.go b/internal/output/output.go index df7db9a71..9940f29fa 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -33,7 +33,7 @@ func BuildFor(o *ast.Output, logger *logger.Logger) (Output, error) { if err := checkOutputGroupUnset(o); err != nil { return nil, err } - return NewPrefixed(logger, o.Prefix.Color), nil + return NewPrefixed(logger), nil default: return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name) } diff --git a/internal/output/output_test.go b/internal/output/output_test.go index 99fc3cc22..9a0c22335 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -113,7 +113,7 @@ func TestPrefixed(t *testing.T) { Color: false, } - var o output.Output = output.NewPrefixed(l, false) + var o output.Output = output.NewPrefixed(l) w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil) t.Run("simple use cases", func(t *testing.T) { @@ -147,7 +147,7 @@ func TestPrefixedWithColor(t *testing.T) { Color: true, } - var o output.Output = output.NewPrefixed(l, true) + var o output.Output = output.NewPrefixed(l) writers := make([]io.Writer, 16) for i := range writers { diff --git a/internal/output/prefixed.go b/internal/output/prefixed.go index 58386f9e1..8898f249b 100644 --- a/internal/output/prefixed.go +++ b/internal/output/prefixed.go @@ -14,17 +14,15 @@ type Prefixed struct { logger *logger.Logger seen map[string]uint counter *uint - color bool } -func NewPrefixed(logger *logger.Logger, color bool) Prefixed { +func NewPrefixed(logger *logger.Logger) Prefixed { var counter uint return Prefixed{ seen: make(map[string]uint), counter: &counter, logger: logger, - color: color, } } @@ -100,14 +98,8 @@ func (pw *prefixWriter) writeLine(line string) error { return nil } - if pw.prefixed.color { - color := PrefixColorSequence[idx%uint(len(PrefixColorSequence))] - pw.prefixed.logger.FOutf(pw.writer, color, pw.prefix) - } else { - if _, err := fmt.Fprint(pw.writer, pw.prefix); err != nil { - return nil - } - } + color := PrefixColorSequence[idx%uint(len(PrefixColorSequence))] + pw.prefixed.logger.FOutf(pw.writer, color, pw.prefix) if _, err := fmt.Fprint(pw.writer, "] "); err != nil { return nil diff --git a/taskfile/ast/output.go b/taskfile/ast/output.go index d3fd0294e..79c4c113f 100644 --- a/taskfile/ast/output.go +++ b/taskfile/ast/output.go @@ -12,8 +12,6 @@ type Output struct { Name string `yaml:"-"` // Group specific style Group OutputGroup - // Prefix specific style - Prefix OutputPrefix } // IsSet returns true if and only if a custom output style is set. @@ -34,33 +32,19 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error { case yaml.MappingNode: var tmp struct { - Group *OutputGroup - Prefixed *OutputPrefix + Group *OutputGroup } - if err := node.Decode(&tmp); err != nil { - return fmt.Errorf("task: output style must be a string or mapping with a \"group\" or \"prefixed\" key: %w", err) + return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err) } - - if tmp.Group != nil { - *s = Output{ - Name: "group", - Group: *tmp.Group, - } - - return nil + if tmp.Group == nil { + return fmt.Errorf("task: output style must have the \"group\" key when in mapping form") } - - if tmp.Prefixed != nil { - *s = Output{ - Name: "prefixed", - Prefix: *tmp.Prefixed, - } - - return nil + *s = Output{ + Name: "group", + Group: *tmp.Group, } - - return fmt.Errorf("task: output style must have either \"group\" or \"prefixed\" key when in mapping form") + return nil } return fmt.Errorf("yaml: line %d: cannot unmarshal %s into output", node.Line, node.ShortTag()) @@ -79,8 +63,3 @@ func (g *OutputGroup) IsSet() bool { } return g.Begin != "" || g.End != "" } - -// OutputPrefix is the style options specific to the Prefix style. -type OutputPrefix struct { - Color bool `yaml:"color"` -} diff --git a/website/docs/api_reference.mdx b/website/docs/api_reference.mdx index 7443dbdf2..43f7ff49c 100644 --- a/website/docs/api_reference.mdx +++ b/website/docs/api_reference.mdx @@ -42,7 +42,6 @@ If `--` is given, all remaining arguments will be assigned to a special | | `--output-group-begin` | `string` | | Message template to print before a task's grouped output. | | | `--output-group-end` | `string` | | Message template to print after a task's grouped output. | | | `--output-group-error-only` | `bool` | `false` | Swallow command output on zero exit code. | -| | `--output-prefix-color` | `bool` | `false` | Use colors for task prefixes. | | `-p` | `--parallel` | `bool` | `false` | Executes tasks provided on command line in parallel. | | `-s` | `--silent` | `bool` | `false` | Disables echoing. | | `-y` | `--yes` | `bool` | `false` | Assume "yes" as answer to all prompts. | diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx index 0462bc8a4..50a010e16 100644 --- a/website/docs/usage.mdx +++ b/website/docs/usage.mdx @@ -1885,22 +1885,6 @@ $ task default [print-baz] baz ``` -When using the `prefix` output, you can optionally configure it to colorize each prefix. -The chosen color is deterministic for a given set of tasks, where the color will cycle -through a predefined sequence of colors. If more tasks are run than colors available, the -color will cycle back and repeat. This can be useful for visually distinguishing tasks in -the output when using the `prefix` output. -```yaml -version: '3' - -output: - prefixed: - color: true - -tasks: - # ... -``` - :::tip The `output` option can also be specified by the `--output` or `-o` flags. diff --git a/website/static/schema.json b/website/static/schema.json index c820f0d16..fb5c3364e 100644 --- a/website/static/schema.json +++ b/website/static/schema.json @@ -544,16 +544,6 @@ "default": false } } - }, - "prefixed": { - "type": "object", - "properties": { - "color": { - "description": "Use colors for task prefixes.", - "type": "boolean", - "default": false - } - } } } },