Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Colorize Atmos Describe Commands when TTY attached #919

Merged
merged 14 commits into from
Jan 16, 2025
16 changes: 11 additions & 5 deletions atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,17 @@ settings:

# Terminal settings for displaying content
terminal:
max_width: 120 # Maximum width for terminal output
pager: true # Use pager for long output
timestamps: false # Show timestamps in logs
colors: true # Enable colored output
unicode: true # Use unicode characters
max_width: 120 # Maximum width for terminal output
pager: true # Pager setting for all terminal output
colors: true # Enable colored output
unicode: true # Use unicode characters

syntax_highlighting:
enabled: true
formatter: terminal # Output formatter (e.g., terminal, html)
theme: dracula # Highlighting theme
line_numbers: true # Display line numbers
wrap: false # Wrap long lines

# Markdown element styling
markdown:
Expand Down
8 changes: 4 additions & 4 deletions cmd/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,13 @@ var docsCmd = &cobra.Command{
u.LogErrorAndExit(schema.AtmosConfiguration{}, err)
}

usePager := atmosConfig.Settings.Terminal.Pager
if !usePager && atmosConfig.Settings.Docs.Pagination {
usePager = atmosConfig.Settings.Docs.Pagination
pager := atmosConfig.Settings.Terminal.Pager
if !pager && atmosConfig.Settings.Docs.Pagination {
pager = atmosConfig.Settings.Docs.Pagination
u.LogWarning(atmosConfig, "'settings.docs.pagination' is deprecated and will be removed in a future version. Please use 'settings.terminal.pager' instead")
}

if err := u.DisplayDocs(componentDocs, usePager); err != nil {
if err := u.DisplayDocs(componentDocs, pager); err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, fmt.Errorf("failed to display documentation: %w", err))
}

Expand Down
6 changes: 3 additions & 3 deletions internal/tui/templates/templater.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ func SetCustomUsageFunc(cmd *cobra.Command) error {
return nil
}

// getTerminalWidth returns the width of the terminal, defaulting to 80 if it cannot be determined
func getTerminalWidth() int {
// GetTerminalWidth returns the width of the terminal, defaulting to 80 if it cannot be determined
func GetTerminalWidth() int {
defaultWidth := 80
screenWidth := defaultWidth

Expand All @@ -156,7 +156,7 @@ func getTerminalWidth() int {
// WrappedFlagUsages formats the flag usage string to fit within the terminal width
func WrappedFlagUsages(f *pflag.FlagSet) string {
var builder strings.Builder
width := getTerminalWidth()
width := GetTerminalWidth()
printer, err := NewHelpFlagPrinter(&builder, uint(width), f)
if err != nil {
// If we can't create the printer, return empty string
Expand Down
18 changes: 18 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/viper"

"github.com/cloudposse/atmos/internal/tui/templates"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/ui/theme"
u "github.com/cloudposse/atmos/pkg/utils"
Expand Down Expand Up @@ -53,6 +54,23 @@ var (
UseEKS: true,
},
},
Settings: schema.AtmosSettings{
ListMergeStrategy: "replace",
Terminal: schema.Terminal{
MaxWidth: templates.GetTerminalWidth(),
Pager: true,
Colors: true,
Unicode: true,
SyntaxHighlighting: schema.SyntaxHighlighting{
Enabled: true,
Formatter: "terminal",
Theme: "dracula",
HighlightedOutputPager: true,
LineNumbers: true,
Wrap: false,
},
},
},
Workflows: schema.Workflows{
BasePath: "stacks/workflows",
},
Expand Down
20 changes: 15 additions & 5 deletions pkg/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,21 @@ type AtmosConfiguration struct {
}

type Terminal struct {
MaxWidth int `yaml:"max_width" json:"max_width" mapstructure:"max_width"`
Pager bool `yaml:"pager" json:"pager" mapstructure:"pager"`
Timestamps bool `yaml:"timestamps" json:"timestamps" mapstructure:"timestamps"`
Colors bool `yaml:"colors" json:"colors" mapstructure:"colors"`
Unicode bool `yaml:"unicode" json:"unicode" mapstructure:"unicode"`
MaxWidth int `yaml:"max_width" json:"max_width" mapstructure:"max_width"`
Pager bool `yaml:"pager" json:"pager" mapstructure:"pager"`
Cerebrovinny marked this conversation as resolved.
Show resolved Hide resolved
Colors bool `yaml:"colors" json:"colors" mapstructure:"colors"`
Unicode bool `yaml:"unicode" json:"unicode" mapstructure:"unicode"`
SyntaxHighlighting SyntaxHighlighting `yaml:"syntax_highlighting" json:"syntax_highlighting" mapstructure:"syntax_highlighting"`
}

type SyntaxHighlighting struct {
Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
Lexer string `yaml:"lexer" json:"lexer" mapstructure:"lexer"`
Formatter string `yaml:"formatter" json:"formatter" mapstructure:"formatter"`
Theme string `yaml:"theme" json:"theme" mapstructure:"theme"`
HighlightedOutputPager bool `yaml:"pager" json:"pager" mapstructure:"pager"`
LineNumbers bool `yaml:"line_numbers" json:"line_numbers" mapstructure:"line_numbers"`
Wrap bool `yaml:"wrap" json:"wrap" mapstructure:"wrap"`
}

type AtmosSettings struct {
Expand Down
17 changes: 17 additions & 0 deletions pkg/utils/config_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package utils

import "github.com/cloudposse/atmos/pkg/schema"

// ExtractAtmosConfig extracts the Atmos configuration from any data type.
// It handles both direct AtmosConfiguration instances and pointers to AtmosConfiguration.
// If the data is neither, it returns an empty configuration.
func ExtractAtmosConfig(data any) schema.AtmosConfiguration {
switch v := data.(type) {
case schema.AtmosConfiguration:
return v
case *schema.AtmosConfiguration:
return *v
default:
return schema.AtmosConfiguration{}
}
}
192 changes: 192 additions & 0 deletions pkg/utils/highlight_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package utils

import (
"bytes"
"io"
"os"
"strings"

"encoding/json"

"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/quick"
"github.com/alecthomas/chroma/styles"
"github.com/cloudposse/atmos/internal/tui/templates"
"github.com/cloudposse/atmos/pkg/schema"
"golang.org/x/term"
)

// DefaultHighlightSettings returns the default syntax highlighting settings
func DefaultHighlightSettings() *schema.SyntaxHighlighting {
return &schema.SyntaxHighlighting{
Enabled: true,
Formatter: "terminal",
Theme: "dracula",
HighlightedOutputPager: true,
LineNumbers: true,
Wrap: false,
}
}

// GetHighlightSettings returns the syntax highlighting settings from the config or defaults
func GetHighlightSettings(config schema.AtmosConfiguration) *schema.SyntaxHighlighting {
defaults := DefaultHighlightSettings()
if config.Settings.Terminal.SyntaxHighlighting == (schema.SyntaxHighlighting{}) {
return defaults
}
settings := &config.Settings.Terminal.SyntaxHighlighting
// Apply defaults for any unset fields
if !settings.Enabled {
settings.Enabled = defaults.Enabled
}
if settings.Formatter == "" {
settings.Formatter = defaults.Formatter
}
if settings.Theme == "" {
settings.Theme = defaults.Theme
}
if !settings.HighlightedOutputPager {
settings.HighlightedOutputPager = defaults.HighlightedOutputPager
}
if !settings.LineNumbers {
settings.LineNumbers = defaults.LineNumbers
}
if !settings.Wrap {
settings.Wrap = defaults.Wrap
}
return settings
}

// HighlightCode highlights the given code using chroma with the specified lexer and theme
func HighlightCode(code string, lexerName string, theme string) (string, error) {
if !term.IsTerminal(int(os.Stdout.Fd())) {
return code, nil
}
var buf bytes.Buffer
err := quick.Highlight(&buf, code, lexerName, "terminal", theme)
if err != nil {
return code, err
}
return buf.String(), nil
}

// HighlightCodeWithConfig highlights the given code using the provided configuration
func HighlightCodeWithConfig(code string, config schema.AtmosConfiguration, format ...string) (string, error) {
if !term.IsTerminal(int(os.Stdout.Fd())) {
return code, nil
}
settings := GetHighlightSettings(config)
if !settings.Enabled {
return code, nil
}

// Get terminal width
config.Settings.Terminal.MaxWidth = templates.GetTerminalWidth()

// Determine lexer based on format flag or content format
var lexerName string
if len(format) > 0 && format[0] != "" {
// Use format flag if provided
lexerName = strings.ToLower(format[0])
} else {
// This is just a fallback
trimmed := strings.TrimSpace(code)

// Try to parse as JSON first
if json.Valid([]byte(trimmed)) {
lexerName = "json"
} else {
// Check for common YAML indicators
// 1. Contains key-value pairs with colons
// 2. Does not start with a curly brace (which could indicate malformed JSON)
// 3. Contains indentation or list markers
if (strings.Contains(trimmed, ":") && !strings.HasPrefix(trimmed, "{")) ||
strings.Contains(trimmed, "\n ") ||
strings.Contains(trimmed, "\n- ") {
lexerName = "yaml"
} else {
// Fallback to plaintext if format is unclear
lexerName = "plaintext"
}
}
}

// Get lexer
lexer := lexers.Get(lexerName)
if lexer == nil {
lexer = lexers.Fallback
}
// Get style
s := styles.Get(settings.Theme)
if s == nil {
s = styles.Fallback
}
// Get formatter
var formatter chroma.Formatter
if settings.LineNumbers {
formatter = formatters.TTY256
} else {
formatter = formatters.Get(settings.Formatter)
if formatter == nil {
formatter = formatters.Fallback
}
}
// Create buffer for output
var buf bytes.Buffer
// Format the code
iterator, err := lexer.Tokenise(nil, code)
if err != nil {
return code, err
}
err = formatter.Format(&buf, s, iterator)
if err != nil {
return code, err
}
return buf.String(), nil
}

// HighlightWriter returns an io.Writer that highlights code written to it
type HighlightWriter struct {
config schema.AtmosConfiguration
writer io.Writer
format string
}

// NewHighlightWriter creates a new HighlightWriter
func NewHighlightWriter(w io.Writer, config schema.AtmosConfiguration, format ...string) *HighlightWriter {
var f string
if len(format) > 0 {
f = format[0]
}
return &HighlightWriter{
config: config,
writer: w,
format: f,
}
}

// Write implements io.Writer
// The returned byte count n is the length of p regardless of whether the highlighting
// process changes the actual number of bytes written to the underlying writer.
// This maintains compatibility with the io.Writer interface contract while still
// providing syntax highlighting functionality.
func (h *HighlightWriter) Write(p []byte) (n int, err error) {
highlighted, err := HighlightCodeWithConfig(string(p), h.config, h.format)
if err != nil {
return 0, err
}

// Write the highlighted content, ignoring the actual number of bytes written
// since we'll return the original input length
_, err = h.writer.Write([]byte(highlighted))
if err != nil {
// If there's an error, we can't be sure how many bytes were actually written
return 0, err
}

// Return the original length of p as required by io.Writer interface
// This ensures that the caller knows all bytes from p were processed
return len(p), nil
}
9 changes: 8 additions & 1 deletion pkg/utils/json_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ func PrintAsJSON(data any) error {
return err
}

PrintMessage(prettyJSON.String())
atmosConfig := ExtractAtmosConfig(data)
highlighted, err := HighlightCodeWithConfig(prettyJSON.String(), atmosConfig)
if err != nil {
// Fallback to plain text if highlighting fails
PrintMessage(prettyJSON.String())
return nil
}
PrintMessage(highlighted)
return nil
}

Expand Down
10 changes: 9 additions & 1 deletion pkg/utils/yaml_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ func PrintAsYAML(data any) error {
if err != nil {
return err
}
PrintMessage(y)

atmosConfig := ExtractAtmosConfig(data)
highlighted, err := HighlightCodeWithConfig(y, atmosConfig)
if err != nil {
// Fallback to plain text if highlighting fails
PrintMessage(y)
return nil
}
PrintMessage(highlighted)
return nil
}

Expand Down
1 change: 0 additions & 1 deletion website/docs/cli/configuration/markdown-styling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ settings:
terminal:
max_width: 120 # Maximum width for terminal output
pager: true # Use pager for long output
timestamps: false
colors: true
unicode: true

Expand Down
Loading
Loading