Skip to content

Commit

Permalink
fix: add better colors control (#812)
Browse files Browse the repository at this point in the history
* fix: add better colors control

* fix: support NO_COLOR and CLICOLOR_FORCE

* fix: use ENV values only if --colors option not set
  • Loading branch information
mrexox authored Sep 2, 2024
1 parent 2df5428 commit 72de5de
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 28 deletions.
11 changes: 10 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func newRootCmd() *cobra.Command {
rootCmd.PersistentFlags().BoolVarP(
&options.Verbose, "verbose", "v", false, "verbose output",
)

rootCmd.PersistentFlags().StringVar(
&options.Colors, "colors", "auto", "'auto', 'on', or 'off'",
)

rootCmd.PersistentFlags().BoolVar(
&options.NoColors, "no-colors", false, "disable colored output",
)
Expand All @@ -42,7 +47,11 @@ func newRootCmd() *cobra.Command {
&options.Aggressive, "aggressive", "a", false,
"use --force flag instead",
)
err := rootCmd.Flags().MarkDeprecated("aggressive", "use command-specific --force option")
err := rootCmd.PersistentFlags().MarkDeprecated("no-colors", "use --colors")
if err != nil {
log.Warn("Unexpected error:", err)
}
err = rootCmd.Flags().MarkDeprecated("aggressive", "use command-specific --force option")
if err != nil {
log.Warn("Unexpected error:", err)
}
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ When set to `true`, fail (with exit status 1) if `lefthook` executable can't be

### `colors`

**Default: `true`**
**Default: `auto`**

Whether enable or disable colorful output of Lefthook. This option can be overwritten with `--no-colors` option. You can also provide your own color codes.
Whether enable or disable colorful output of Lefthook. This option can be overwritten with `--colors` option. You can also provide your own color codes.

**Example**

Expand Down
10 changes: 10 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Then use git as usually, you don't need to reinstall lefthook when you change th
- [`LEFTHOOK_QUIET`](#lefthook_quiet)
- [`LEFTHOOK_VERBOSE`](#lefthook_verbose)
- [`LEFTHOOK_BIN`](#lefthook_bin)
- [`NO_COLOR`](#no_color)
- [`CLICOLOR_FORCE`](#clicolor_force)
- [Features and tips](#features-and-tips)
- [Disable lefthook in CI](#disable-lefthook-in-ci)
- [Local config](#local-config)
Expand Down Expand Up @@ -215,6 +217,14 @@ Useful for cases when:
- lefthook is installed multiple ways, and you want to be explicit about which one is used (example: installed through homebrew, but also is in Gemfile but you are using a ruby version manager like rbenv that prepends it to the path)
- debugging and/or developing lefthook

### `NO_COLOR`

Set `NO_COLOR=true` to disable colored output in lefthook and all subcommands that lefthook calls.

### `CLICOLOR_FORCE`

Set `CLICOLOR_FORCE=true` to force colored output in lefthook and all subcommands.

## Features and tips

### Disable lefthook in CI
Expand Down
31 changes: 29 additions & 2 deletions internal/lefthook/lefthook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (

const (
EnvVerbose = "LEFTHOOK_VERBOSE" // keep all output
envNoColor = "NO_COLOR"
envForceColor = "CLICOLOR_FORCE"
hookFileMode = 0o755
oldHookPostfix = ".old"
)
Expand All @@ -26,6 +28,7 @@ var lefthookContentRegexp = regexp.MustCompile("LEFTHOOK")
type Options struct {
Fs afero.Fs
Verbose, NoColors bool
Colors string

// DEPRECATED. Will be removed in 1.3.0.
Force, Aggressive bool
Expand All @@ -41,15 +44,30 @@ type Lefthook struct {

// New returns an instance of Lefthook.
func initialize(opts *Options) (*Lefthook, error) {
if os.Getenv(EnvVerbose) == "1" || os.Getenv(EnvVerbose) == "true" {
if isEnvEnabled(EnvVerbose) {
opts.Verbose = true
}

if opts.Verbose {
log.SetLevel(log.DebugLevel)
}

log.SetColors(!opts.NoColors)
if opts.Colors == "auto" {
if isEnvEnabled(envForceColor) {
opts.Colors = "on"
}

if isEnvEnabled(envNoColor) {
opts.Colors = "off"
}

// DEPRECATED: Will be removed with a --no-colors option
if opts.NoColors {
opts.Colors = "off"
}
}

log.SetColors(opts.Colors)

repo, err := git.NewRepository(opts.Fs, git.NewExecutor(system.Cmd))
if err != nil {
Expand Down Expand Up @@ -124,3 +142,12 @@ func (l *Lefthook) addHook(hook string, args templates.Args) error {
l.Fs, hookPath, templates.Hook(hook, args), hookFileMode,
)
}

func isEnvEnabled(name string) bool {
value := os.Getenv(name)
if len(value) > 0 && value != "0" && value != "false" {
return true
}

return false
}
15 changes: 3 additions & 12 deletions internal/lefthook/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/evilmartians/lefthook/internal/config"
"github.com/evilmartians/lefthook/internal/lefthook/runner"
"github.com/evilmartians/lefthook/internal/log"
"github.com/evilmartians/lefthook/internal/version"
)

const (
Expand Down Expand Up @@ -91,10 +90,7 @@ func (l *Lefthook) Run(hookName string, args RunArgs, gitArgs []string) error {
// }

if logSettings.LogMeta() {
log.Box(
log.Cyan("🥊 lefthook ")+log.Gray(fmt.Sprintf("v%s", version.Version(false))),
log.Gray("hook: ")+log.Bold(hookName),
)
log.LogMeta(hookName)
}

if !args.NoAutoInstall {
Expand Down Expand Up @@ -231,7 +227,7 @@ func printSummary(
continue
}

log.Infof("✔️ %s\n", log.Green(result.Name))
log.Success(result.Name)
}
}

Expand All @@ -241,12 +237,7 @@ func printSummary(
continue
}

failText := result.Text()
if len(failText) != 0 {
failText = fmt.Sprintf(": %s", failText)
}

log.Infof("🥊 %s%s\n", log.Red(result.Name), log.Red(failText))
log.Failure(result.Name, result.Text())
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions internal/lefthook/runner/exec/execute_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ func (e CommandExecutor) Execute(ctx context.Context, opts Options, in io.Reader
fmt.Sprintf("%s=%s", strings.ToUpper(name), os.ExpandEnv(value)),
)
}
switch log.Colors() {
case log.ColorOn:
envs = append(envs, "CLICOLOR_FORCE=true")
case log.ColorOff:
envs = append(envs, "NO_COLOR=true")
}

args := &executeArgs{
in: in,
Expand Down
7 changes: 7 additions & 0 deletions internal/lefthook/runner/exec/execute_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"syscall"

"github.com/evilmartians/lefthook/internal/log"

"github.com/mattn/go-isatty"
"github.com/mattn/go-tty"
)
Expand Down Expand Up @@ -42,6 +43,12 @@ func (e CommandExecutor) Execute(ctx context.Context, opts Options, in io.Reader
fmt.Sprintf("%s=%s", strings.ToUpper(name), os.ExpandEnv(value)),
)
}
switch log.Colors() {
case log.ColorOn:
envs = append(envs, "CLICOLOR_FORCE=true")
case log.ColorOff:
envs = append(envs, "NO_COLOR=true")
}

args := &executeArgs{
in: in,
Expand Down
88 changes: 77 additions & 11 deletions internal/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

"github.com/briandowns/spinner"
"github.com/charmbracelet/lipgloss"

"github.com/evilmartians/lefthook/internal/version"
)

var (
Expand Down Expand Up @@ -38,6 +40,10 @@ const (
spinnerCharSet = 14
spinnerRefreshRate = 100 * time.Millisecond
spinnerText = " waiting"

ColorAuto = iota
ColorOn
ColorOff
)

type StyleLogger struct {
Expand All @@ -48,7 +54,7 @@ type Logger struct {
level Level
out io.Writer
mu sync.Mutex
colors bool
colors int
names []string
spinner *spinner.Spinner
}
Expand All @@ -57,7 +63,7 @@ func New() *Logger {
return &Logger{
level: InfoLevel,
out: os.Stdout,
colors: true,
colors: ColorAuto,
spinner: spinner.New(
spinner.CharSets[spinnerCharSet],
spinnerRefreshRate,
Expand All @@ -66,6 +72,14 @@ func New() *Logger {
}
}

func Colors() int {
return std.colors
}

func Colorized() bool {
return std.colors == ColorAuto || std.colors == ColorOn
}

func StartSpinner() {
std.spinner.Start()
}
Expand Down Expand Up @@ -159,29 +173,49 @@ func SetLevel(level Level) {
}

func SetColors(colors interface{}) {
if colors == nil {
return
}

switch typedColors := colors.(type) {
case bool:
std.colors = typedColors
if !std.colors {
case string:
switch typedColors {
case "on":
std.colors = ColorOn
case "off":
std.colors = ColorOff
setColor(lipgloss.NoColor{}, &ColorRed)
setColor(lipgloss.NoColor{}, &ColorGreen)
setColor(lipgloss.NoColor{}, &ColorYellow)
setColor(lipgloss.NoColor{}, &ColorCyan)
setColor(lipgloss.NoColor{}, &GolorGray)
setColor(lipgloss.NoColor{}, &colorBorder)
default:
std.colors = ColorAuto
}
return
case bool:
if typedColors {
std.colors = ColorOn
return
}

std.colors = ColorOff
setColor(lipgloss.NoColor{}, &ColorRed)
setColor(lipgloss.NoColor{}, &ColorGreen)
setColor(lipgloss.NoColor{}, &ColorYellow)
setColor(lipgloss.NoColor{}, &ColorCyan)
setColor(lipgloss.NoColor{}, &GolorGray)
setColor(lipgloss.NoColor{}, &colorBorder)
case map[string]interface{}:
std.colors = true
std.colors = ColorOn
setColor(typedColors["red"], &ColorRed)
setColor(typedColors["green"], &ColorGreen)
setColor(typedColors["yellow"], &ColorYellow)
setColor(typedColors["cyan"], &ColorCyan)
setColor(typedColors["gray"], &GolorGray)
setColor(typedColors["gray"], &colorBorder)
return
default:
std.colors = true
std.colors = ColorAuto
}
}

Expand Down Expand Up @@ -227,14 +261,46 @@ func Gray(s string) string {
}

func Bold(s string) string {
if !std.colors {
if !Colorized() {
return lipgloss.NewStyle().Render(s)
}

return lipgloss.NewStyle().Bold(true).Render(s)
}

func Box(left, right string) {
func LogMeta(hookName string) {
name := "🥊 lefthook "
if !Colorized() {
name = "lefthook "
}

box(
Cyan(name)+Gray(fmt.Sprintf("v%s", version.Version(false))),
Gray("hook: ")+Bold(hookName),
)
}

func Success(name string) {
format := "✔️ %s\n"
if !Colorized() {
format = "✓ %s\n"
}
Infof(format, Green(name))
}

func Failure(name, failText string) {
if len(failText) != 0 {
failText = fmt.Sprintf(": %s", failText)
}

format := "🥊 %s%s\n"
if !Colorized() {
format = "✗ %s%s\n"
}
Infof(format, Red(name), Red(failText))
}

func box(left, right string) {
Info(
lipgloss.JoinHorizontal(
lipgloss.Top,
Expand Down

0 comments on commit 72de5de

Please sign in to comment.