diff --git a/command.go b/command.go index a446845..b33edb0 100644 --- a/command.go +++ b/command.go @@ -10,9 +10,10 @@ import ( "strings" "unicode/utf8" - "go.followtheprocess.codes/cli/internal/colour" "go.followtheprocess.codes/cli/internal/flag" - "go.followtheprocess.codes/cli/internal/table" + "go.followtheprocess.codes/cli/internal/style" + + "go.followtheprocess.codes/hue/tabwriter" ) const ( @@ -518,9 +519,9 @@ func defaultHelp(cmd *Command) error { s.WriteString("\n\n") } - s.WriteString(colour.Title("Usage")) + s.WriteString(style.Title.Text("Usage")) s.WriteString(": ") - s.WriteString(colour.Bold(cmd.name)) + s.WriteString(style.Bold.Text(cmd.name)) if len(cmd.subcommands) == 0 { // We don't have any subcommands so usage will be: @@ -570,7 +571,7 @@ func defaultHelp(cmd *Command) error { s.WriteString("\n\n") } - s.WriteString(colour.Title("Options")) + s.WriteString(style.Title.Text("Options")) s.WriteString(":\n\n") s.WriteString(usage) @@ -609,22 +610,22 @@ func writePositionalArgs(cmd *Command, s *strings.Builder) { // text string builder. func writeArgumentsSection(cmd *Command, s *strings.Builder) error { s.WriteString("\n\n") - s.WriteString(colour.Title("Arguments")) + s.WriteString(style.Title.Text("Arguments")) s.WriteString(":\n") - tab := table.New(s) + tw := tabwriter.NewWriter(s, style.MinWidth, style.TabWidth, style.Padding, style.PadChar, style.Flags) for _, arg := range cmd.positionalArgs { switch arg.defaultValue { case requiredArgMarker: - tab.Row(" %s\t%s\t[required]\n", colour.Bold(arg.name), arg.description) + fmt.Fprintf(tw, " %s\t%s\t[required]\n", style.Bold.Text(arg.name), arg.description) case "": - tab.Row(" %s\t%s\t[default %q]\n", colour.Bold(arg.name), arg.description, arg.defaultValue) + fmt.Fprintf(tw, " %s\t%s\t[default %q]\n", style.Bold.Text(arg.name), arg.description, arg.defaultValue) default: - tab.Row(" %s\t%s\t[default %s]\n", colour.Bold(arg.name), arg.description, arg.defaultValue) + fmt.Fprintf(tw, " %s\t%s\t[default %s]\n", style.Bold.Text(arg.name), arg.description, arg.defaultValue) } } - if err := tab.Flush(); err != nil { + if err := tw.Flush(); err != nil { return fmt.Errorf("could not format arguments: %w", err) } @@ -641,7 +642,7 @@ func writeExamples(cmd *Command, s *strings.Builder) { s.WriteString("\n\n") } - s.WriteString(colour.Title("Examples")) + s.WriteString(style.Title.Text("Examples")) s.WriteByte(':') s.WriteString("\n\n") @@ -672,16 +673,16 @@ func writeSubcommands(cmd *Command, s *strings.Builder) error { s.WriteString("\n\n") } - s.WriteString(colour.Title("Commands")) + s.WriteString(style.Title.Text("Commands")) s.WriteByte(':') s.WriteString("\n\n") - tab := table.New(s) + tw := tabwriter.NewWriter(s, style.MinWidth, style.TabWidth, style.Padding, style.PadChar, style.Flags) for _, subcommand := range cmd.subcommands { - tab.Row(" %s\t%s\n", colour.Bold(subcommand.name), subcommand.short) + fmt.Fprintf(tw, " %s\t%s\n", style.Bold.Text(subcommand.name), subcommand.short) } - if err := tab.Flush(); err != nil { + if err := tw.Flush(); err != nil { return fmt.Errorf("could not format subcommands: %w", err) } @@ -707,22 +708,22 @@ func defaultVersion(cmd *Command) error { s := &strings.Builder{} s.Grow(versionBufferSize) - s.WriteString(colour.Title(cmd.name)) + s.WriteString(style.Title.Text(cmd.name)) s.WriteString("\n\n") - s.WriteString(colour.Bold("Version:")) + s.WriteString(style.Bold.Text("Version:")) s.WriteString(" ") s.WriteString(cmd.version) s.WriteString("\n") if cmd.commit != "" { - s.WriteString(colour.Bold("Commit:")) + s.WriteString(style.Bold.Text("Commit:")) s.WriteString(" ") s.WriteString(cmd.commit) s.WriteString("\n") } if cmd.buildDate != "" { - s.WriteString(colour.Bold("BuildDate:")) + s.WriteString(style.Bold.Text("BuildDate:")) s.WriteString(" ") s.WriteString(cmd.buildDate) s.WriteString("\n") diff --git a/docs/img/namedargs.gif b/docs/img/namedargs.gif index cdd087c..61a5b51 100644 Binary files a/docs/img/namedargs.gif and b/docs/img/namedargs.gif differ diff --git a/docs/img/quickstart.gif b/docs/img/quickstart.gif index 331811b..98bdcc4 100644 Binary files a/docs/img/quickstart.gif and b/docs/img/quickstart.gif differ diff --git a/docs/img/subcommands.gif b/docs/img/subcommands.gif index 92dd27d..c1c247b 100644 Binary files a/docs/img/subcommands.gif and b/docs/img/subcommands.gif differ diff --git a/go.mod b/go.mod index edf048a..176c010 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,12 @@ ignore ( ) require ( + go.followtheprocess.codes/hue v1.0.0 go.followtheprocess.codes/snapshot v0.6.1 go.followtheprocess.codes/test v0.23.1 ) require ( - go.followtheprocess.codes/hue v0.7.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/term v0.35.0 // indirect ) diff --git a/go.sum b/go.sum index 586502a..5747e78 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -go.followtheprocess.codes/hue v0.7.0 h1:6HaZTGc4NYVnqqjYZTTBcVYRKb8MyfPe5yBHJiBfekg= -go.followtheprocess.codes/hue v0.7.0/go.mod h1:gSn5xK6KJapih+eFgQk3woo1qg3/rx9XSrAanUIuDr8= +go.followtheprocess.codes/hue v1.0.0 h1:0fYXAGR1o+w7Vja+Q+iVtqeEP3/CE6ET/pniyl8e9yo= +go.followtheprocess.codes/hue v1.0.0/go.mod h1:gSn5xK6KJapih+eFgQk3woo1qg3/rx9XSrAanUIuDr8= go.followtheprocess.codes/snapshot v0.6.1 h1:cZkQtEjL21BSrHn98Lm0S4yTzcOje/K60Iog/u/A5tM= go.followtheprocess.codes/snapshot v0.6.1/go.mod h1:CM2E92Ah/j0XL4Z2UyOl7GlSuD0ZLLl8rJCpFylKcIg= go.followtheprocess.codes/test v0.23.1 h1:VoucCC8qKb6tKnBOCRZ7Ln2Ex1oV+HMXHdZyJ6DURB8= diff --git a/internal/colour/colour.go b/internal/colour/colour.go deleted file mode 100644 index 628b164..0000000 --- a/internal/colour/colour.go +++ /dev/null @@ -1,77 +0,0 @@ -// Package colour implements basic text colouring for cli'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. -package colour - -import ( - "os" - "sync" - "sync/atomic" -) - -// ANSI codes for coloured output, they are all the same length so as not to throw off -// alignment of [text/tabwriter]. -const ( - CodeReset = "\x1b[000000m" // Reset all attributes - CodeTitle = "\x1b[1;39;4m" // Bold, white & underlined - CodeBold = "\x1b[1;0039m" // Bold & white -) - -// Disable is a flag that disables all colour text, it overrides both -// $FORCE_COLOR and $NO_COLOR, setting it to true will always make this -// package return plain text and not check any other config. -var Disable atomic.Bool - -// 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, forceColour bool) { - no := os.Getenv("NO_COLOR") != "" - force := os.Getenv("FORCE_COLOR") != "" - - return no, force -} - -// Title returns the given text in a title style, bold white and underlined. -// -// If $NO_COLOR is set, text will be returned unmodified. -func Title(text string) string { - return sprint(CodeTitle, text) -} - -// Bold returns the given text in bold white. -// -// If $NO_COLOR is set, text will be returned unmodified. -func Bold(text string) string { - return sprint(CodeBold, text) -} - -// sprint returns a string with a given colour and the reset code. -// -// It handles checking for NO_COLOR and FORCE_COLOR. If the global var -// [Disable] is true then nothing else is checked and plain text is returned. -func sprint(code, text string) string { - // Our global variable is above all else - if Disable.Load() { - return text - } - - noColor, forceColor := getColourOnce() - - // $FORCE_COLOR overrides $NO_COLOR - if forceColor { - return code + text + CodeReset - } - - // $NO_COLOR is next - if noColor { - return text - } - - // Normal - return code + text + CodeReset -} diff --git a/internal/flag/set.go b/internal/flag/set.go index 6e0381e..799f5c9 100644 --- a/internal/flag/set.go +++ b/internal/flag/set.go @@ -7,8 +7,8 @@ import ( "slices" "strings" - "go.followtheprocess.codes/cli/internal/colour" - "go.followtheprocess.codes/cli/internal/table" + "go.followtheprocess.codes/cli/internal/style" + "go.followtheprocess.codes/hue/tabwriter" ) // usageBufferSize is sufficient to hold most commands flag usage text. @@ -199,7 +199,7 @@ func (s *Set) Usage() (string, error) { slices.Sort(names) - tab := table.New(buf) + tw := tabwriter.NewWriter(buf, style.MinWidth, style.TabWidth, style.Padding, style.PadChar, style.Flags) for _, name := range names { flag := s.flags[name] @@ -214,16 +214,17 @@ func (s *Set) Usage() (string, error) { shorthand = "N/A" } - tab.Row( + fmt.Fprintf( + tw, " %s\t--%s\t%s\t%s\n", - colour.Bold(shorthand), - colour.Bold(name), + style.Bold.Text(shorthand), + style.Bold.Text(name), flag.Type(), flag.Usage(), ) } - if err := tab.Flush(); err != nil { + if err := tw.Flush(); err != nil { return "", fmt.Errorf("could not format flags: %w", err) } diff --git a/internal/flag/set_test.go b/internal/flag/set_test.go index bc418db..65f56db 100644 --- a/internal/flag/set_test.go +++ b/internal/flag/set_test.go @@ -6,7 +6,6 @@ import ( "slices" "testing" - "go.followtheprocess.codes/cli/internal/colour" "go.followtheprocess.codes/cli/internal/flag" "go.followtheprocess.codes/snapshot" "go.followtheprocess.codes/test" @@ -1304,8 +1303,6 @@ func TestUsage(t *testing.T) { snap := snapshot.New(t, snapshot.Update(*update)) set := tt.newSet(t) - colour.Disable.Store(true) // For testing - got, err := set.Usage() test.Ok(t, err) diff --git a/internal/style/style.go b/internal/style/style.go new file mode 100644 index 0000000..e546790 --- /dev/null +++ b/internal/style/style.go @@ -0,0 +1,30 @@ +// Package style simply provides a uniform terminal printing style via [hue] for use across +// the library. +// +// [hue]: https://github.com/FollowTheProcess/hue +package style + +import "go.followtheprocess.codes/hue" + +const ( + // Title is the style for titles of help text sections like arguments or commands. + Title = hue.Bold | hue.White | hue.Underline + + // Bold is simply plain bold text. + Bold = hue.Bold + + // MinWidth is the minimum cell width for hue's colour-enabled tabwriter. + MinWidth = 1 + + // TabWidth is the width of tabs in spaces for tabwriter. + TabWidth = 8 + + // Padding is the number of PadChars to pad table cells with. + Padding = 2 + + // PadChar is the character with which to pad table cells. + PadChar = ' ' + + // Flags is the tabwriter config flags. + Flags = 0 +) diff --git a/internal/table/table.go b/internal/table/table.go deleted file mode 100644 index 9654471..0000000 --- a/internal/table/table.go +++ /dev/null @@ -1,42 +0,0 @@ -// Package table implements a thin wrapper around [text/tabwriter] to keep -// formatting consistent across cli. -package table - -import ( - "fmt" - "io" - "text/tabwriter" -) - -// TableWriter config, used for showing subcommands in help. -const ( - minWidth = 1 // Min cell width - tabWidth = 8 // Tab width in spaces - padding = 2 // Padding - padChar = ' ' // Char to pad with - flags = 0 // Config flags -) - -// Table is a text table. -type Table struct { - tw *tabwriter.Writer // The underlying writer -} - -// New returns a new [Table], writing to w. -func New(w io.Writer) Table { - tw := tabwriter.NewWriter(w, minWidth, tabWidth, padding, padChar, flags) - - return Table{tw: tw} -} - -// Row adds a row to the [Table]. -// -//nolint:goprintffuncname // I like it this way -func (t Table) Row(format string, a ...any) { - fmt.Fprintf(t.tw, format, a...) -} - -// Flush flushes the written data to the writer. -func (t Table) Flush() error { - return t.tw.Flush() -} diff --git a/internal/table/table_test.go b/internal/table/table_test.go deleted file mode 100644 index 1a94e55..0000000 --- a/internal/table/table_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package table_test - -import ( - "bytes" - "flag" - "fmt" - "testing" - - "go.followtheprocess.codes/cli/internal/table" - "go.followtheprocess.codes/snapshot" - "go.followtheprocess.codes/test" -) - -var ( - debug = flag.Bool("debug", false, "Print debug output during tests") - update = flag.Bool("update", false, "Update golden files") -) - -func TestTable(t *testing.T) { - snap := snapshot.New(t, snapshot.Update(*update)) - buf := &bytes.Buffer{} - - tab := table.New(buf) - - tab.Row("Col1\tCol2\tCol3\n") - tab.Row("val1\tval2\tval3\n") - tab.Row("val4\tval5\tval6\n") - - err := tab.Flush() - test.Ok(t, err) - - if *debug { - fmt.Printf("DEBUG (%s)\n_____\n\n%s\n", "TestTable", buf.String()) - } - - snap.Snap(buf.String()) -} diff --git a/internal/table/testdata/snapshots/TestTable.snap.txt b/internal/table/testdata/snapshots/TestTable.snap.txt deleted file mode 100644 index cac2330..0000000 --- a/internal/table/testdata/snapshots/TestTable.snap.txt +++ /dev/null @@ -1,3 +0,0 @@ -Col1 Col2 Col3 -val1 val2 val3 -val4 val5 val6 diff --git a/option.go b/option.go index b3246fe..c0f209f 100644 --- a/option.go +++ b/option.go @@ -7,8 +7,8 @@ import ( "slices" "strings" - "go.followtheprocess.codes/cli/internal/colour" "go.followtheprocess.codes/cli/internal/flag" + "go.followtheprocess.codes/hue" ) // NoShortHand should be passed as the "short" argument to [Flag] if the desired flag @@ -190,7 +190,7 @@ func Stderr(stderr io.Writer) Option { func NoColour(noColour bool) Option { f := func(_ *config) error { // Just disable the internal colour package entirely - colour.Disable.Store(noColour) + hue.Enabled(!noColour) return nil }