mirror of
				https://github.com/go-task/task.git
				synced 2025-10-30 23:58:01 +02:00 
			
		
		
		
	feat: colorize tasks in prefixed output (#1572)
* feat: Colorize tasks in prefixed output * chore: comment and style changes * fix code tag has spaces in api reference * fix: migrate to use logger for colors * fix: Add bright colors to the color sequence * fix: make colorized prefix logger standard
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							0810ef01b0
						
					
				
				
					commit
					856ba3b8c2
				
			| @@ -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 | ||||
|   | ||||
| @@ -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) (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) (Output, error) { | ||||
| 		if err := checkOutputGroupUnset(o); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return Prefixed{}, nil | ||||
| 		return NewPrefixed(logger), nil | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name) | ||||
| 	} | ||||
|   | ||||
| @@ -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.Prefixed{} | ||||
| 	l := &logger.Logger{ | ||||
| 		Color: 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) { | ||||
| @@ -132,3 +138,33 @@ func TestPrefixed(t *testing.T) { | ||||
| 		assert.Equal(t, "[prefix] Test!\n", b.String()) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestPrefixedWithColor(t *testing.T) { | ||||
| 	color.NoColor = false | ||||
|  | ||||
| 	var b bytes.Buffer | ||||
| 	l := &logger.Logger{ | ||||
| 		Color: true, | ||||
| 	} | ||||
|  | ||||
| 	var o output.Output = output.NewPrefixed(l) | ||||
|  | ||||
| 	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)] | ||||
|  | ||||
| 			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.String(), prefix.String()), b.String()) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -6,20 +6,36 @@ import ( | ||||
| 	"io" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/internal/logger" | ||||
| 	"github.com/go-task/task/v3/internal/templater" | ||||
| ) | ||||
|  | ||||
| type Prefixed struct{} | ||||
| type Prefixed struct { | ||||
| 	logger  *logger.Logger | ||||
| 	seen    map[string]uint | ||||
| 	counter *uint | ||||
| } | ||||
|  | ||||
| func (Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) { | ||||
| 	pw := &prefixWriter{writer: stdOut, prefix: prefix} | ||||
| func NewPrefixed(logger *logger.Logger) Prefixed { | ||||
| 	var counter uint | ||||
|  | ||||
| 	return Prefixed{ | ||||
| 		seen:    make(map[string]uint), | ||||
| 		counter: &counter, | ||||
| 		logger:  logger, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (p Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) { | ||||
| 	pw := &prefixWriter{writer: stdOut, prefix: prefix, prefixed: &p} | ||||
| 	return pw, pw, func(error) error { return pw.close() } | ||||
| } | ||||
|  | ||||
| type prefixWriter struct { | ||||
| 	writer io.Writer | ||||
| 	prefix string | ||||
| 	buff   bytes.Buffer | ||||
| 	writer   io.Writer | ||||
| 	prefixed *Prefixed | ||||
| 	prefix   string | ||||
| 	buff     bytes.Buffer | ||||
| } | ||||
|  | ||||
| func (pw *prefixWriter) Write(p []byte) (int, error) { | ||||
| @@ -56,6 +72,11 @@ func (pw *prefixWriter) writeOutputLines(force bool) error { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var PrefixColorSequence = []logger.Color{ | ||||
| 	logger.Yellow, logger.Blue, logger.Magenta, logger.Cyan, logger.Green, logger.Red, | ||||
| 	logger.BrightYellow, logger.BrightBlue, logger.BrightMagenta, logger.BrightCyan, logger.BrightGreen, logger.BrightRed, | ||||
| } | ||||
|  | ||||
| func (pw *prefixWriter) writeLine(line string) error { | ||||
| 	if line == "" { | ||||
| 		return nil | ||||
| @@ -63,6 +84,27 @@ 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.prefixed.seen[pw.prefix] | ||||
|  | ||||
| 	if !ok { | ||||
| 		idx = *pw.prefixed.counter | ||||
| 		pw.prefixed.seen[pw.prefix] = idx | ||||
|  | ||||
| 		*pw.prefixed.counter++ | ||||
| 	} | ||||
|  | ||||
| 	if _, err := fmt.Fprint(pw.writer, "["); 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 | ||||
| 	} | ||||
|  | ||||
| 	_, err := fmt.Fprint(pw.writer, line) | ||||
| 	return err | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user