diff --git a/codecov.yml b/codecov.yml index 6fa9caa..16baf9a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,3 @@ ignore: - docs # Demo programs to showcase the library, coverage tracking not needed + - internal # Tested via the library diff --git a/go.mod b/go.mod index 04496cd..f1d1aee 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,3 @@ module github.com/FollowTheProcess/msg -go 1.22 - -require github.com/fatih/color v1.17.0 - -require ( - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/sys v0.24.0 // indirect -) +go 1.23 diff --git a/go.sum b/go.sum index e04f2c9..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +0,0 @@ -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/colour/colour.go b/internal/colour/colour.go new file mode 100644 index 0000000..c410dd3 --- /dev/null +++ b/internal/colour/colour.go @@ -0,0 +1,81 @@ +// Package colour implements basic text colouring for msg's limited needs. +// +// In particular, it's not expected to provide every ANSI code, just the ones we need. The codes have also been padded so that they are +// the same length, which means [text/tabwriter] will correctly calculate alignment as long as styles are not mixed within a table. +// +// The functions in this package respect both $NO_COLOR and $FORCE_COLOR. +package colour + +import ( + "os" + "sync" +) + +// ANSI codes for coloured output, they are all the same length so as not to throw off +// alignment of [text/tabwriter]. +const ( + CodeError = "\x1b[1;0031m" // Bold red, used for the error status + CodeTitle = "\x1b[1;0096m" // Bold hi cyan, used for titles + CodeInfo = "\x1b[1;0036m" // Bold cyan, used for info + CodeWarn = "\x1b[1;0033m" // Bold yellow, used for warn + CodeSuccess = "\x1b[1;0032m" // Bold geen, used for success + CodeReset = "\x1b[000000m" // Reset all attributes +) + +// getColourOnce is a [sync.OnceValues] function that returns the state of +// $NO_COLOR and $FORCE_COLOR, once and only once to avoid us calling +// os.Getenv on every call to a colour function. +var getColourOnce = sync.OnceValues(getColour) + +// getColour returns whether $NO_COLOR and $FORCE_COLOR were set. +func getColour() (noColour bool, forceColour bool) { + no := os.Getenv("NO_COLOR") != "" + force := os.Getenv("FORCE_COLOR") != "" + + return no, force +} + +// Title returns a title styled string. +func Title(text string) string { + return sprint(CodeTitle, text) +} + +// Success returns a success styled string. +func Success(text string) string { + return sprint(CodeSuccess, text) +} + +// Info returns an info styled string. +func Info(text string) string { + return sprint(CodeInfo, text) +} + +// Warn returns a warn styled string. +func Warn(text string) string { + return sprint(CodeWarn, text) +} + +// Error returns an error styled string. +func Error(text string) string { + return sprint(CodeError, text) +} + +// sprint returns a string with a given colour and the reset code. +// +// It handles checking for NO_COLOR and FORCE_COLOR. +func sprint(code, text string) string { + no, force := getColourOnce() + + // $FORCE_COLOR overrides $NO_COLOR + if force { + return code + text + CodeReset + } + + // $NO_COLOR is next + if no { + return text + } + + // Normal + return code + text + CodeReset +} diff --git a/msg.go b/msg.go index 5c7c0aa..818136b 100644 --- a/msg.go +++ b/msg.go @@ -17,17 +17,10 @@ import ( "io" "os" - "github.com/fatih/color" + "github.com/FollowTheProcess/msg/internal/colour" ) const ( - // Default colors for each message type. - colorInfo = color.FgHiCyan - colorTitle = color.FgCyan - colorWarn = color.FgYellow - colorError = color.FgHiRed - colorSuccess = color.FgGreen - // Default statuses for each message type. statusInfo = "Info" statusWarn = "Warning" @@ -37,8 +30,6 @@ const ( // Success prints a success message with optional format args to stdout. // -// # Example -// // msg.Success("Compiled project: %s", "msg") func Success(format string, a ...any) { Fsuccess(os.Stdout, format, a...) @@ -46,20 +37,13 @@ func Success(format string, a ...any) { // Fsuccess prints a success message with optional format args to w. // -// # Example -// // msg.Fsuccess(os.Stdout, "Compiled project: %s", "msg") func Fsuccess(w io.Writer, format string, a ...any) { - success := color.New(colorSuccess, color.Bold) - message := color.New(color.FgWhite) - - fmt.Fprintf(w, "%s: %s\n", success.Sprint(statusSuccess), message.Sprintf(format, a...)) + fmt.Fprintf(w, "%s: %s\n", colour.Success(statusSuccess), fmt.Sprintf(format, a...)) } // Error prints an error message with optional format args to stderr. // -// # Example -// // msg.Error("Invalid config") // msg.Error("Could not find file: %s", "missing.txt") func Error(format string, a ...any) { @@ -68,20 +52,13 @@ func Error(format string, a ...any) { // Ferror prints an error message with optional format args to w. // -// # Example -// // msg.Ferror(os.Stderr, "Uh oh! %s", "something wrong") func Ferror(w io.Writer, format string, a ...any) { - e := color.New(colorError, color.Bold) - message := color.New(color.FgWhite) - - fmt.Fprintf(w, "%s: %s\n", e.Sprint(statusError), message.Sprintf(format, a...)) + fmt.Fprintf(w, "%s: %s\n", colour.Error(statusError), fmt.Sprintf(format, a...)) } // Warn prints a warning message with optional format args to stdout. // -// # Example -// // msg.Warn("Skipping %s, directory is empty", "some/empty/dir") func Warn(format string, a ...any) { Fwarn(os.Stdout, format, a...) @@ -89,20 +66,13 @@ func Warn(format string, a ...any) { // Fwarn prints a warning message with optional format args to w. // -// # Example -// // msg.Fwarn(os.Stderr, "hmmmm: %v", true) func Fwarn(w io.Writer, format string, a ...any) { - warn := color.New(colorWarn, color.Bold) - message := color.New(color.FgWhite) - - fmt.Fprintf(w, "%s: %s\n", warn.Sprint(statusWarn), message.Sprintf(format, a...)) + fmt.Fprintf(w, "%s: %s\n", colour.Warn(statusWarn), fmt.Sprintf(format, a...)) } // Info prints an info message with optional format args to stdout. // -// # Example -// // msg.Info("You have %d repos on GitHub", 42) func Info(format string, a ...any) { Finfo(os.Stdout, format, a...) @@ -110,14 +80,9 @@ func Info(format string, a ...any) { // Finfo prints an info message with optional format args to w. // -// # Example -// // msg.Finfo(os.Stdout, "The meaning of life is %v", 42) func Finfo(w io.Writer, format string, a ...any) { - info := color.New(colorInfo, color.Bold) - message := color.New(color.FgWhite) - - fmt.Fprintf(w, "%s: %s\n", info.Sprint(statusInfo), message.Sprintf(format, a...)) + fmt.Fprintf(w, "%s: %s\n", colour.Info(statusInfo), fmt.Sprintf(format, a...)) } // Title prints a title message to stdout. @@ -126,8 +91,6 @@ func Finfo(w io.Writer, format string, a ...any) { // has 1 leading newline and 2 trailing newlines to create separation between // the sections it is differentiating in your CLI. // -// # Example -// // msg.Title("Some section") func Title(format string, a ...any) { Ftitle(os.Stdout, format, a...) @@ -139,11 +102,7 @@ func Title(format string, a ...any) { // has 1 leading newline and 2 trailing newlines to create separation between // the sections it is differentiating in your CLI. // -// # Example -// // msg.Ftitle(os.Stdout, "Some section") func Ftitle(w io.Writer, format string, a ...any) { - title := color.New(colorTitle, color.Bold) - - fmt.Fprintf(w, "\n%s\n\n", title.Sprintf(format, a...)) + fmt.Fprintf(w, "\n%s\n\n", colour.Title(fmt.Sprintf(format, a...))) } diff --git a/msg_test.go b/msg_test.go index a0e7505..3afba33 100644 --- a/msg_test.go +++ b/msg_test.go @@ -2,18 +2,20 @@ package msg_test import ( "bytes" + "fmt" "io" "os" "testing" "github.com/FollowTheProcess/msg" + "github.com/FollowTheProcess/msg/internal/colour" ) func TestSuccess(t *testing.T) { buf := new(bytes.Buffer) msg.Fsuccess(buf, "Something went well: %v", 42) - want := "Success: Something went well: 42\n" + want := fmt.Sprintf("%sSuccess%s: Something went well: 42\n", colour.CodeSuccess, colour.CodeReset) if got := buf.String(); got != want { t.Errorf("got %q, wanted %q", got, want) @@ -25,7 +27,7 @@ func TestSuccessCaptured(t *testing.T) { msg.Success("Worked") } got := captureStdout(t, successFunc) - want := "Success: Worked\n" + want := fmt.Sprintf("%sSuccess%s: Worked\n", colour.CodeSuccess, colour.CodeReset) if got != want { t.Errorf("got %q, wanted %q", got, want) @@ -36,7 +38,7 @@ func TestError(t *testing.T) { buf := new(bytes.Buffer) msg.Ferror(buf, "Something broke: %v", true) - want := "Error: Something broke: true\n" + want := fmt.Sprintf("%sError%s: Something broke: true\n", colour.CodeError, colour.CodeReset) if got := buf.String(); got != want { t.Errorf("got %q, wanted %q", got, want) @@ -48,7 +50,7 @@ func TestErrorCaptured(t *testing.T) { msg.Error("Bad number (%v)", 42) } got := captureStderr(t, errorFunc) - want := "Error: Bad number (42)\n" + want := fmt.Sprintf("%sError%s: Bad number (42)\n", colour.CodeError, colour.CodeReset) if got != want { t.Errorf("got %q, wanted %q", got, want) @@ -59,7 +61,7 @@ func TestWarn(t *testing.T) { buf := new(bytes.Buffer) msg.Fwarn(buf, "skipping directory %s", "./tmp") - want := "Warning: skipping directory ./tmp\n" + want := fmt.Sprintf("%sWarning%s: skipping directory ./tmp\n", colour.CodeWarn, colour.CodeReset) if got := buf.String(); got != want { t.Errorf("got %q, wanted %q", got, want) @@ -71,7 +73,7 @@ func TestWarnCaptured(t *testing.T) { msg.Warn("Skipping something (%d)", 42) } got := captureStdout(t, warnFunc) - want := "Warning: Skipping something (42)\n" + want := fmt.Sprintf("%sWarning%s: Skipping something (42)\n", colour.CodeWarn, colour.CodeReset) if got != want { t.Errorf("got %q, wanted %q", got, want) @@ -82,7 +84,7 @@ func TestInfo(t *testing.T) { buf := new(bytes.Buffer) msg.Finfo(buf, "You have %d projects on GitHub", 27) - want := "Info: You have 27 projects on GitHub\n" + want := fmt.Sprintf("%sInfo%s: You have 27 projects on GitHub\n", colour.CodeInfo, colour.CodeReset) if got := buf.String(); got != want { t.Errorf("got %q, wanted %q", got, want) @@ -94,7 +96,7 @@ func TestInfoCaptured(t *testing.T) { msg.Info("You are %d years old", 29) } got := captureStdout(t, infoFunc) - want := "Info: You are 29 years old\n" + want := fmt.Sprintf("%sInfo%s: You are 29 years old\n", colour.CodeInfo, colour.CodeReset) if got != want { t.Errorf("got %q, wanted %q", got, want) @@ -105,7 +107,7 @@ func TestTitle(t *testing.T) { buf := new(bytes.Buffer) msg.Ftitle(buf, "Section Header") - want := "\nSection Header\n\n" + want := fmt.Sprintf("\n%sSection Header%s\n\n", colour.CodeTitle, colour.CodeReset) if got := buf.String(); got != want { t.Errorf("got %q, wanted %q", got, want) @@ -117,7 +119,7 @@ func TestTitleCaptured(t *testing.T) { msg.Title("Section Header") } got := captureStdout(t, titleFunc) - want := "\nSection Header\n\n" + want := fmt.Sprintf("\n%sSection Header%s\n\n", colour.CodeTitle, colour.CodeReset) if got != want { t.Errorf("got %q, wanted %q", got, want)