This commit is contained in:
visrosa 2026-05-23 08:41:09 -03:00 committed by GitHub
commit 56bf64686d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 129 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -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")
}
}

48
main.go
View file

@ -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)