fs: Add global flag '--color' to control terminal colors

* fs: add TerminalColorMode type
* fs: add new config(flags) for TerminalColorMode
* lib/terminal: use TerminalColorMode to determine how to handle colors
* Add documentation for '--terminal-color-mode'
* tree: remove obsolete --color replaced by global --color

This changes the default behaviour of tree. It now displays colors by
default instead of only displaying them when the flag -C/--color was
active. Old behaviour (no color) can be achieved by setting --color to
'never'.

Fixes: #6604
This commit is contained in:
Kevin Verstaen 2022-12-06 13:07:06 +01:00 committed by GitHub
parent a9bd0c8de6
commit c2dfc3e5b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 157 additions and 4 deletions

View File

@ -61,7 +61,6 @@ func init() {
flags.StringVarP(cmdFlags, &sort, "sort", "", "", "Select sort: name,version,size,mtime,ctime") flags.StringVarP(cmdFlags, &sort, "sort", "", "", "Select sort: name,version,size,mtime,ctime")
// Graphics // Graphics
flags.BoolVarP(cmdFlags, &opts.NoIndent, "noindent", "", false, "Don't print indentation lines") flags.BoolVarP(cmdFlags, &opts.NoIndent, "noindent", "", false, "Don't print indentation lines")
flags.BoolVarP(cmdFlags, &opts.Colorize, "color", "C", false, "Turn colorization on always")
} }
var commandDefinition = &cobra.Command{ var commandDefinition = &cobra.Command{
@ -116,6 +115,7 @@ For a more interactive navigation of the remote see the
opts.SizeSort = sort == "size" opts.SizeSort = sort == "size"
ci := fs.GetConfig(context.Background()) ci := fs.GetConfig(context.Background())
opts.UnitSize = ci.HumanReadable opts.UnitSize = ci.HumanReadable
opts.Colorize = ci.TerminalColorMode != fs.TerminalColorModeNever
if opts.DeepLevel == 0 { if opts.DeepLevel == 0 {
opts.DeepLevel = ci.MaxDepth opts.DeepLevel = ci.MaxDepth
} }

View File

@ -48,7 +48,6 @@ rclone tree remote:path [flags]
``` ```
-a, --all All files are listed (list . files too) -a, --all All files are listed (list . files too)
-C, --color Turn colorization on always
-d, --dirs-only List directories only -d, --dirs-only List directories only
--dirsfirst List directories before files (-U disables) --dirsfirst List directories before files (-U disables)
--full-path Print the full path prefix for each file --full-path Print the full path prefix for each file

View File

@ -823,6 +823,16 @@ quicker than without the `--checksum` flag.
When using this flag, rclone won't update mtimes of remote files if When using this flag, rclone won't update mtimes of remote files if
they are incorrect as it would normally. they are incorrect as it would normally.
### --color WHEN ###
Specifiy when colors (and other ANSI codes) should be added to the output.
`AUTO` (default) only allows ANSI codes when the output is a terminal
`NEVER` never allow ANSI codes
`ALWAYS` always add ANSI codes, regardless of the output format (terminal or file)
### --compare-dest=DIR ### ### --compare-dest=DIR ###
When using `sync`, `copy` or `move` DIR is checked in addition to the When using `sync`, `copy` or `move` DIR is checked in addition to the

View File

@ -27,6 +27,7 @@ These flags are available for every command.
-c, --checksum Skip based on checksum (if available) & size, not mod-time & size -c, --checksum Skip based on checksum (if available) & size, not mod-time & size
--client-cert string Client SSL certificate (PEM) for mutual TLS auth --client-cert string Client SSL certificate (PEM) for mutual TLS auth
--client-key string Client SSL private key (PEM) for mutual TLS auth --client-key string Client SSL private key (PEM) for mutual TLS auth
--color Define when colors (and other ANSI codes) should be shown AUTO|ALWAYS|NEVER (default AUTO)
--compare-dest stringArray Include additional comma separated server-side paths during comparison --compare-dest stringArray Include additional comma separated server-side paths during comparison
--config string Config file (default "$HOME/.config/rclone/rclone.conf") --config string Config file (default "$HOME/.config/rclone/rclone.conf")
--contimeout duration Connect timeout (default 1m0s) --contimeout duration Connect timeout (default 1m0s)

View File

@ -142,6 +142,7 @@ type ConfigInfo struct {
DisableHTTPKeepAlives bool DisableHTTPKeepAlives bool
Metadata bool Metadata bool
ServerSideAcrossConfigs bool ServerSideAcrossConfigs bool
TerminalColorMode TerminalColorMode
} }
// NewConfig creates a new config with everything set to the default // NewConfig creates a new config with everything set to the default

View File

@ -142,6 +142,7 @@ func AddFlags(ci *fs.ConfigInfo, flagSet *pflag.FlagSet) {
flags.BoolVarP(flagSet, &ci.DisableHTTPKeepAlives, "disable-http-keep-alives", "", ci.DisableHTTPKeepAlives, "Disable HTTP keep-alives and use each connection once.") flags.BoolVarP(flagSet, &ci.DisableHTTPKeepAlives, "disable-http-keep-alives", "", ci.DisableHTTPKeepAlives, "Disable HTTP keep-alives and use each connection once.")
flags.BoolVarP(flagSet, &ci.Metadata, "metadata", "M", ci.Metadata, "If set, preserve metadata when copying objects") flags.BoolVarP(flagSet, &ci.Metadata, "metadata", "M", ci.Metadata, "If set, preserve metadata when copying objects")
flags.BoolVarP(flagSet, &ci.ServerSideAcrossConfigs, "server-side-across-configs", "", ci.ServerSideAcrossConfigs, "Allow server-side operations (e.g. copy) to work across different configs") flags.BoolVarP(flagSet, &ci.ServerSideAcrossConfigs, "server-side-across-configs", "", ci.ServerSideAcrossConfigs, "Allow server-side operations (e.g. copy) to work across different configs")
flags.FVarP(flagSet, &ci.TerminalColorMode, "color", "", "When to show colors (and other ANSI codes) AUTO|NEVER|ALWAYS")
} }
// ParseHeaders converts the strings passed in via the header flags into HTTPOptions // ParseHeaders converts the strings passed in via the header flags into HTTPOptions

57
fs/terminalcolormode.go Normal file
View File

@ -0,0 +1,57 @@
package fs
import (
"fmt"
"strings"
)
// TerminalColorMode describes how ANSI codes should be handled
type TerminalColorMode byte
// TerminalColorMode constants
const (
TerminalColorModeAuto TerminalColorMode = iota
TerminalColorModeNever
TerminalColorModeAlways
)
var terminalColorModeToString = []string{
TerminalColorModeAuto: "AUTO",
TerminalColorModeNever: "NEVER",
TerminalColorModeAlways: "ALWAYS",
}
// String converts a TerminalColorMode to a string
func (m TerminalColorMode) String() string {
if m >= TerminalColorMode(len(terminalColorModeToString)) {
return fmt.Sprintf("TerminalColorMode(%d)", m)
}
return terminalColorModeToString[m]
}
// Set a TerminalColorMode
func (m *TerminalColorMode) Set(s string) error {
for n, name := range terminalColorModeToString {
if s != "" && name == strings.ToUpper(s) {
*m = TerminalColorMode(n)
return nil
}
}
return fmt.Errorf("unknown terminal color mode %q", s)
}
// Type of TerminalColorMode
func (m TerminalColorMode) Type() string {
return "string"
}
// UnmarshalJSON converts a string/integer in JSON to a TerminalColorMode
func (m *TerminalColorMode) UnmarshalJSON(in []byte) error {
return UnmarshalJSONFlag(in, m, func(i int64) error {
if i < 0 || i >= int64(len(terminalColorModeToString)) {
return fmt.Errorf("out of range terminal color mode %d", i)
}
*m = (TerminalColorMode)(i)
return nil
})
}

View File

@ -0,0 +1,74 @@
package fs
import (
"encoding/json"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTerminalColorModeString(t *testing.T) {
for _, test := range []struct {
in TerminalColorMode
want string
}{
{TerminalColorModeAuto, "AUTO"},
{TerminalColorModeAlways, "ALWAYS"},
{TerminalColorModeNever, "NEVER"},
{36, "TerminalColorMode(36)"},
} {
tcm := test.in
assert.Equal(t, test.want, tcm.String(), test.in)
}
}
func TestTerminalColorModeSet(t *testing.T) {
for _, test := range []struct {
in string
want TerminalColorMode
expectError bool
}{
{"auto", TerminalColorModeAuto, false},
{"ALWAYS", TerminalColorModeAlways, false},
{"Never", TerminalColorModeNever, false},
{"INVALID", 0, true},
} {
tcm := TerminalColorMode(0)
err := tcm.Set(test.in)
if test.expectError {
require.Error(t, err, test.in)
} else {
require.NoError(t, err, test.in)
}
assert.Equal(t, test.want, tcm, test.in)
}
}
func TestTerminalColorModeUnmarshalJSON(t *testing.T) {
for _, test := range []struct {
in string
want TerminalColorMode
expectError bool
}{
{`"auto"`, TerminalColorModeAuto, false},
{`"ALWAYS"`, TerminalColorModeAlways, false},
{`"Never"`, TerminalColorModeNever, false},
{`"Invalid"`, 0, true},
{strconv.Itoa(int(TerminalColorModeAuto)), TerminalColorModeAuto, false},
{strconv.Itoa(int(TerminalColorModeAlways)), TerminalColorModeAlways, false},
{strconv.Itoa(int(TerminalColorModeNever)), TerminalColorModeNever, false},
{`99`, 0, true},
{`-99`, 0, true},
} {
var tcm TerminalColorMode
err := json.Unmarshal([]byte(test.in), &tcm)
if test.expectError {
require.Error(t, err, test.in)
} else {
require.NoError(t, err, test.in)
}
assert.Equal(t, test.want, tcm, test.in)
}
}

View File

@ -3,12 +3,14 @@
package terminal package terminal
import ( import (
"context"
"io" "io"
"os" "os"
"runtime" "runtime"
"sync" "sync"
colorable "github.com/mattn/go-colorable" colorable "github.com/mattn/go-colorable"
"github.com/rclone/rclone/fs"
) )
// VT100 codes // VT100 codes
@ -73,13 +75,21 @@ var (
// Start the terminal - must be called before use // Start the terminal - must be called before use
func Start() { func Start() {
once.Do(func() { once.Do(func() {
ci := fs.GetConfig(context.Background())
f := os.Stdout f := os.Stdout
if !IsTerminal(int(f.Fd())) { if !IsTerminal(int(f.Fd())) {
// If stdout not a tty then remove escape codes // If stdout is not a tty, remove escape codes EXCEPT if terminal color mode equals "ALWAYS"
Out = colorable.NewNonColorable(f) if ci.TerminalColorMode == fs.TerminalColorModeAlways {
Out = colorable.NewColorable(f)
} else {
Out = colorable.NewNonColorable(f)
}
} else if runtime.GOOS == "windows" && os.Getenv("TERM") != "" { } else if runtime.GOOS == "windows" && os.Getenv("TERM") != "" {
// If TERM is set just use stdout // If TERM is set just use stdout
Out = f Out = f
} else if ci.TerminalColorMode == fs.TerminalColorModeNever {
Out = colorable.NewNonColorable(f)
} else { } else {
Out = colorable.NewColorable(f) Out = colorable.NewColorable(f)
} }