From 1e735b28efdc5d2e4afce416f4c0e2868d9ac145 Mon Sep 17 00:00:00 2001 From: rosvi Date: Sat, 23 May 2026 08:19:01 -0300 Subject: [PATCH 1/3] feat: add color mode support Adds --color=[auto|always]. Forces the renderer profile to TrueColor when color=always --- main.go | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index b31ca15..25d7d33 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" gap "github.com/muesli/go-app-paths" + "github.com/muesli/termenv" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/term" @@ -44,6 +45,7 @@ var ( showLineNumbers bool preserveNewLines bool mouse bool + colorMode string rootCmd = &cobra.Command{ Use: "glow [SOURCE|DIR]", @@ -164,8 +166,36 @@ func validateStyle(style string) error { return nil } +func validateColorMode(mode string) error { + switch mode { + case "auto", "always": + return nil + default: + return fmt.Errorf("invalid color mode %q: must be one of auto or always", mode) + } +} + +func resolveStyleForColorMode(mode string, currentStyle string, isTerminal bool, styleFlagChanged bool) string { + if mode != "always" || styleFlagChanged || currentStyle != styles.AutoStyle || isTerminal { + return currentStyle + } + if lipgloss.HasDarkBackground() { + return styles.DarkStyle + } + return styles.LightStyle +} + +func shouldUseNoTTYStyle(mode string, isTerminal bool, styleFlagChanged bool) bool { + return mode != "always" && !isTerminal && !styleFlagChanged +} + func validateOptions(cmd *cobra.Command) error { // grab config values from Viper + colorMode = viper.GetString("color") + if err := validateColorMode(colorMode); err != nil { + return err + } + width = viper.GetUint("width") mouse = viper.GetBool("mouse") pager = viper.GetBool("pager") @@ -185,10 +215,12 @@ func validateOptions(cmd *cobra.Command) error { } isTerminal := term.IsTerminal(int(os.Stdout.Fd())) + styleFlagChanged := cmd.Flags().Changed("style") + style = resolveStyleForColorMode(colorMode, style, isTerminal, styleFlagChanged) // We want to use a special no-TTY style, when stdout is not a terminal - // and there was no specific style passed by arg - if !isTerminal && !cmd.Flags().Changed("style") { - style = "notty" + // and there was no specific style passed by arg. + if shouldUseNoTTYStyle(colorMode, isTerminal, styleFlagChanged) { + style = styles.NoTTYStyle } // Detect terminal width @@ -290,9 +322,14 @@ func executeCLI(cmd *cobra.Command, src *source, w io.Writer) error { isCode := !utils.IsMarkdownFile(src.URL) + colorProfile := lipgloss.ColorProfile() + if colorMode == "always" { + colorProfile = termenv.TrueColor + } + // initialize glamour r, err := glamour.NewTermRenderer( - glamour.WithColorProfile(lipgloss.ColorProfile()), + glamour.WithColorProfile(colorProfile), utils.GlamourStyle(style, isCode), glamour.WithWordWrap(int(width)), //nolint:gosec glamour.WithBaseURL(baseURL), @@ -403,6 +440,7 @@ func init() { rootCmd.Flags().BoolVarP(&pager, "pager", "p", false, "display with pager") rootCmd.Flags().BoolVarP(&tui, "tui", "t", false, "display with tui") rootCmd.Flags().StringVarP(&style, "style", "s", styles.AutoStyle, "style name or JSON path") + rootCmd.Flags().StringVar(&colorMode, "color", "auto", "when to enable colors (auto|always)") rootCmd.Flags().UintVarP(&width, "width", "w", 0, "word-wrap at width (set to 0 to disable)") rootCmd.Flags().BoolVarP(&showAllFiles, "all", "a", false, "show system files and directories (TUI-mode only)") rootCmd.Flags().BoolVarP(&showLineNumbers, "line-numbers", "l", false, "show line numbers (TUI-mode only)") @@ -414,6 +452,7 @@ func init() { _ = viper.BindPFlag("pager", rootCmd.Flags().Lookup("pager")) _ = viper.BindPFlag("tui", rootCmd.Flags().Lookup("tui")) _ = viper.BindPFlag("style", rootCmd.Flags().Lookup("style")) + _ = viper.BindPFlag("color", rootCmd.Flags().Lookup("color")) _ = viper.BindPFlag("width", rootCmd.Flags().Lookup("width")) _ = viper.BindPFlag("debug", rootCmd.Flags().Lookup("debug")) _ = viper.BindPFlag("mouse", rootCmd.Flags().Lookup("mouse")) @@ -422,6 +461,7 @@ func init() { _ = viper.BindPFlag("all", rootCmd.Flags().Lookup("all")) viper.SetDefault("style", styles.AutoStyle) + viper.SetDefault("color", "auto") viper.SetDefault("width", 0) viper.SetDefault("all", true) From b24222e8e31029413b80d6a0812ab4351f7c1a1e Mon Sep 17 00:00:00 2001 From: rosvi Date: Sat, 23 May 2026 08:20:36 -0300 Subject: [PATCH 2/3] test: add color-mode coverage Covers flag parsing, `shouldUseNoTTYStyle`, `resolveStyleForColorMode` and `validateColorMode` --- glow_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/glow_test.go b/glow_test.go index 8743be2..6bace4b 100644 --- a/glow_test.go +++ b/glow_test.go @@ -2,6 +2,9 @@ package main import ( "testing" + + "github.com/charmbracelet/glamour/styles" + "github.com/charmbracelet/lipgloss" ) func TestGlowFlags(t *testing.T) { @@ -27,6 +30,12 @@ func TestGlowFlags(t *testing.T) { return width == 40 }, }, + { + args: []string{"--color", "always"}, + check: func() bool { + return colorMode == "always" + }, + }, } for _, v := range tt { @@ -39,3 +48,69 @@ func TestGlowFlags(t *testing.T) { } } } + +func TestShouldUseNoTTYStyle(t *testing.T) { + cases := []struct { + name string + mode string + isTerminal bool + styleFlag bool + expectNoTTY bool + }{ + {name: "auto non-terminal", mode: "auto", isTerminal: false, expectNoTTY: true}, + {name: "always non-terminal", mode: "always", isTerminal: false, expectNoTTY: false}, + {name: "auto terminal", mode: "auto", isTerminal: true, expectNoTTY: false}, + {name: "auto style flag set", mode: "auto", isTerminal: false, styleFlag: true, expectNoTTY: false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := shouldUseNoTTYStyle(tc.mode, tc.isTerminal, tc.styleFlag); got != tc.expectNoTTY { + t.Fatalf("shouldUseNoTTYStyle(%q, %v, %v) = %v, want %v", tc.mode, tc.isTerminal, tc.styleFlag, got, tc.expectNoTTY) + } + }) + } +} + +func TestResolveStyleForColorMode(t *testing.T) { + cases := []struct { + name string + mode string + currentStyle string + isTerminal bool + styleFlag bool + expectStyle string + }{ + {name: "auto non-terminal keeps auto when style explicit", mode: "always", currentStyle: "dark", isTerminal: false, styleFlag: true, expectStyle: "dark"}, + {name: "auto terminal keeps auto", mode: "always", currentStyle: styles.AutoStyle, isTerminal: true, expectStyle: styles.AutoStyle}, + {name: "auto mode keeps auto", mode: "auto", currentStyle: styles.AutoStyle, isTerminal: false, expectStyle: styles.AutoStyle}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := resolveStyleForColorMode(tc.mode, tc.currentStyle, tc.isTerminal, tc.styleFlag); got != tc.expectStyle { + t.Fatalf("resolveStyleForColorMode(%q, %q, %v, %v) = %q, want %q", tc.mode, tc.currentStyle, tc.isTerminal, tc.styleFlag, got, tc.expectStyle) + } + }) + } + + want := styles.DarkStyle + if !lipgloss.HasDarkBackground() { + want = styles.LightStyle + } + if got := resolveStyleForColorMode("always", styles.AutoStyle, false, false); got != want { + t.Fatalf("resolveStyleForColorMode(always, auto, false, false) = %q, want %q", got, want) + } +} + +func TestValidateColorMode(t *testing.T) { + if err := validateColorMode("auto"); err != nil { + t.Fatalf("expected auto to be valid: %v", err) + } + if err := validateColorMode("always"); err != nil { + t.Fatalf("expected always to be valid: %v", err) + } + if err := validateColorMode("never"); err == nil { + t.Fatal("expected never to be invalid") + } +} From a020293fc206271d8c19ffa23c37d191b1ed16bd Mon Sep 17 00:00:00 2001 From: rosvi Date: Sat, 23 May 2026 08:22:44 -0300 Subject: [PATCH 3/3] docs: document `--color-always` --- README.md | 8 ++++++++ config_cmd.go | 2 ++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index cf3bb15..668be51 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,12 @@ either the `dark` or the `light` style for you. glow -s [dark|light] ``` +For piped output, use `--color=always` to keep colorized output even when stdout is not a terminal. + +```bash +glow --color=always +``` + Alternatively you can also supply a custom JSON stylesheet: ```bash @@ -201,6 +207,8 @@ Here's an example config: ```yaml # style name or JSON path (default "auto") style: "light" +# colorize output when piped (auto|always) +color: "auto" # mouse wheel support (TUI-mode only) mouse: true # use pager to display markdown diff --git a/config_cmd.go b/config_cmd.go index 390a1db..2e6c874 100644 --- a/config_cmd.go +++ b/config_cmd.go @@ -15,6 +15,8 @@ import ( const defaultConfig = `# style name or JSON path (default "auto") style: "auto" +# colorize output when piped (auto|always) +color: "auto" # mouse support (TUI-mode only) mouse: false # use pager to display markdown