From c2dfc3e5b37572eeaef8d2f55fb6bfe3c6a8211e Mon Sep 17 00:00:00 2001 From: Kevin Verstaen <48050031+kverstae@users.noreply.github.com> Date: Tue, 6 Dec 2022 13:07:06 +0100 Subject: [PATCH] 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 --- cmd/tree/tree.go | 2 +- docs/content/commands/rclone_tree.md | 1 - docs/content/docs.md | 10 ++++ docs/content/flags.md | 1 + fs/config.go | 1 + fs/config/configflags/configflags.go | 1 + fs/terminalcolormode.go | 57 +++++++++++++++++++++ fs/terminalcolormode_test.go | 74 ++++++++++++++++++++++++++++ lib/terminal/terminal.go | 14 +++++- 9 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 fs/terminalcolormode.go create mode 100644 fs/terminalcolormode_test.go diff --git a/cmd/tree/tree.go b/cmd/tree/tree.go index ae776b067..ca2999aac 100644 --- a/cmd/tree/tree.go +++ b/cmd/tree/tree.go @@ -61,7 +61,6 @@ func init() { flags.StringVarP(cmdFlags, &sort, "sort", "", "", "Select sort: name,version,size,mtime,ctime") // Graphics 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{ @@ -116,6 +115,7 @@ For a more interactive navigation of the remote see the opts.SizeSort = sort == "size" ci := fs.GetConfig(context.Background()) opts.UnitSize = ci.HumanReadable + opts.Colorize = ci.TerminalColorMode != fs.TerminalColorModeNever if opts.DeepLevel == 0 { opts.DeepLevel = ci.MaxDepth } diff --git a/docs/content/commands/rclone_tree.md b/docs/content/commands/rclone_tree.md index 1995a108f..3ecc450e5 100644 --- a/docs/content/commands/rclone_tree.md +++ b/docs/content/commands/rclone_tree.md @@ -48,7 +48,6 @@ rclone tree remote:path [flags] ``` -a, --all All files are listed (list . files too) - -C, --color Turn colorization on always -d, --dirs-only List directories only --dirsfirst List directories before files (-U disables) --full-path Print the full path prefix for each file diff --git a/docs/content/docs.md b/docs/content/docs.md index 4b58920a2..1afa2a9a0 100644 --- a/docs/content/docs.md +++ b/docs/content/docs.md @@ -823,6 +823,16 @@ quicker than without the `--checksum` flag. When using this flag, rclone won't update mtimes of remote files if 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 ### When using `sync`, `copy` or `move` DIR is checked in addition to the diff --git a/docs/content/flags.md b/docs/content/flags.md index 1cdce0e1d..c94a9e904 100644 --- a/docs/content/flags.md +++ b/docs/content/flags.md @@ -27,6 +27,7 @@ These flags are available for every command. -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-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 --config string Config file (default "$HOME/.config/rclone/rclone.conf") --contimeout duration Connect timeout (default 1m0s) diff --git a/fs/config.go b/fs/config.go index 13d523558..d496b4d88 100644 --- a/fs/config.go +++ b/fs/config.go @@ -142,6 +142,7 @@ type ConfigInfo struct { DisableHTTPKeepAlives bool Metadata bool ServerSideAcrossConfigs bool + TerminalColorMode TerminalColorMode } // NewConfig creates a new config with everything set to the default diff --git a/fs/config/configflags/configflags.go b/fs/config/configflags/configflags.go index 42b2b4d05..abd5209d7 100644 --- a/fs/config/configflags/configflags.go +++ b/fs/config/configflags/configflags.go @@ -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.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.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 diff --git a/fs/terminalcolormode.go b/fs/terminalcolormode.go new file mode 100644 index 000000000..7da58e13f --- /dev/null +++ b/fs/terminalcolormode.go @@ -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 + }) +} diff --git a/fs/terminalcolormode_test.go b/fs/terminalcolormode_test.go new file mode 100644 index 000000000..6060b4342 --- /dev/null +++ b/fs/terminalcolormode_test.go @@ -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) + } +} diff --git a/lib/terminal/terminal.go b/lib/terminal/terminal.go index 4eda1abab..532cf1fcc 100644 --- a/lib/terminal/terminal.go +++ b/lib/terminal/terminal.go @@ -3,12 +3,14 @@ package terminal import ( + "context" "io" "os" "runtime" "sync" colorable "github.com/mattn/go-colorable" + "github.com/rclone/rclone/fs" ) // VT100 codes @@ -73,13 +75,21 @@ var ( // Start the terminal - must be called before use func Start() { once.Do(func() { + ci := fs.GetConfig(context.Background()) + f := os.Stdout if !IsTerminal(int(f.Fd())) { - // If stdout not a tty then remove escape codes - Out = colorable.NewNonColorable(f) + // If stdout is not a tty, remove escape codes EXCEPT if terminal color mode equals "ALWAYS" + if ci.TerminalColorMode == fs.TerminalColorModeAlways { + Out = colorable.NewColorable(f) + } else { + Out = colorable.NewNonColorable(f) + } } else if runtime.GOOS == "windows" && os.Getenv("TERM") != "" { // If TERM is set just use stdout Out = f + } else if ci.TerminalColorMode == fs.TerminalColorModeNever { + Out = colorable.NewNonColorable(f) } else { Out = colorable.NewColorable(f) }