diff --git a/atmos.yaml b/atmos.yaml index 8fffeebba..c3e10fc53 100644 --- a/atmos.yaml +++ b/atmos.yaml @@ -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: diff --git a/cmd/docs.go b/cmd/docs.go index 82bcaae7b..b4f1a8b7b 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -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)) } diff --git a/go.mod b/go.mod index 0a8f9a1fb..4e8a38e62 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/alecthomas/chroma v0.10.0 github.com/arsham/figurine v1.3.0 github.com/aws/aws-sdk-go-v2 v1.32.8 - github.com/aws/aws-sdk-go-v2/config v1.28.9 + github.com/aws/aws-sdk-go-v2/config v1.28.10 github.com/aws/aws-sdk-go-v2/service/ssm v1.56.4 github.com/bmatcuk/doublestar/v4 v4.7.1 github.com/charmbracelet/bubbles v0.20.0 @@ -86,7 +86,7 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go v1.44.206 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.50 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.51 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.14 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 // indirect @@ -100,7 +100,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.26.10 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 // indirect github.com/aws/smithy-go v1.22.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect diff --git a/go.sum b/go.sum index b75178c2d..b7532a094 100644 --- a/go.sum +++ b/go.sum @@ -746,12 +746,12 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 h1:SdK4Ppk5IzLs64ZM github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.15.9/go.mod h1:rv/l/TbZo67kp99v/3Kb0qV6Fm1KEtKyruEV2GvVfgs= -github.com/aws/aws-sdk-go-v2/config v1.28.9 h1:7/P2J1MGkava+2c9Xlk7CTPTpGqFAOaM4874wJsGi4Q= -github.com/aws/aws-sdk-go-v2/config v1.28.9/go.mod h1:ce/HX8tHlIh4VTPaLz/aQIvA5+/rUghFy+nGMrXHQ9U= +github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg= +github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.12.4/go.mod h1:7g+GGSp7xtR823o1jedxKmqRZGqLdoHQfI4eFasKKxs= -github.com/aws/aws-sdk-go-v2/credentials v1.17.50 h1:63pBzfU7EG4RbMMVRv4Hgm34cIaPXICCnHojKdPbTR0= -github.com/aws/aws-sdk-go-v2/credentials v1.17.50/go.mod h1:m5ThO5y87w0fiAHBt9cYXS5BVsebOeJEFCGUQeZZYLw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk= +github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5/go.mod h1:WAPnuhG5IQ/i6DETFl5NmX3kKqCzw7aau9NHAGcm4QE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI= @@ -804,8 +804,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.16.6/go.mod h1:rP1rEOKAGZoXp4iGDxSXFvODAtXpm34Egf0lL0eshaQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.5 h1:URp6kw3vHAnuU9pgP4K1SohwWLDzgtqA/qgeBfgBxn0= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.5/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= diff --git a/internal/exec/vendor_model.go b/internal/exec/vendor_model.go index d4b1848a1..9f91fcc7d 100644 --- a/internal/exec/vendor_model.go +++ b/internal/exec/vendor_model.go @@ -162,7 +162,7 @@ func (m *modelVendor) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } version := grayColor.Render(version) return m, tea.Sequence( - tea.Printf("%s %s %s", mark, pkg.name, version), + tea.Printf("%s %s %s %s", mark, pkg.name, version, errMsg), tea.Quit, ) } diff --git a/internal/exec/vendor_utils.go b/internal/exec/vendor_utils.go index d116f62ac..a9e0fd3aa 100644 --- a/internal/exec/vendor_utils.go +++ b/internal/exec/vendor_utils.go @@ -181,7 +181,16 @@ func ReadAndProcessVendorConfigFile( // Check if it's a directory fileInfo, err := os.Stat(foundVendorConfigFile) if err != nil { - return vendorConfig, false, "", err + if os.IsNotExist(err) { + // File does not exist + return vendorConfig, false, "", fmt.Errorf("Vendoring is not configured. To set up vendoring, please see https://atmos.tools/core-concepts/vendor/") + } + if os.IsPermission(err) { + // Permission error + return vendorConfig, false, "", fmt.Errorf("Permission denied when accessing '%s'. Please check the file permissions.", foundVendorConfigFile) + } + // Other errors + return vendorConfig, false, "", fmt.Errorf("An error occurred while accessing the vendoring configuration: %w", err) } var configFiles []string diff --git a/internal/tui/templates/templater.go b/internal/tui/templates/templater.go index c4539a6ea..468f71329 100644 --- a/internal/tui/templates/templater.go +++ b/internal/tui/templates/templater.go @@ -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 @@ -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 diff --git a/pkg/config/config.go b/pkg/config/config.go index 42495aa93..37ddc13fd 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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" @@ -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", }, diff --git a/pkg/config/utils.go b/pkg/config/utils.go index d78601618..8d658d443 100644 --- a/pkg/config/utils.go +++ b/pkg/config/utils.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/charmbracelet/log" + "github.com/cloudposse/atmos/pkg/logger" "github.com/cloudposse/atmos/pkg/schema" "github.com/cloudposse/atmos/pkg/store" u "github.com/cloudposse/atmos/pkg/utils" @@ -358,6 +359,11 @@ func processEnvVars(atmosConfig *schema.AtmosConfiguration) error { logsLevel := os.Getenv("ATMOS_LOGS_LEVEL") if len(logsLevel) > 0 { u.LogTrace(*atmosConfig, fmt.Sprintf("Found ENV var ATMOS_LOGS_LEVEL=%s", logsLevel)) + // Validate the log level before setting it + if _, err := logger.ParseLogLevel(logsLevel); err != nil { + return err + } + // Only set the log level if validation passes atmosConfig.Logs.Level = logsLevel } @@ -396,6 +402,12 @@ func checkConfig(atmosConfig schema.AtmosConfiguration) error { return errors.New("at least one path must be provided in 'stacks.included_paths' config or ATMOS_STACKS_INCLUDED_PATHS' ENV variable") } + if len(atmosConfig.Logs.Level) > 0 { + if _, err := logger.ParseLogLevel(atmosConfig.Logs.Level); err != nil { + return err + } + } + return nil } @@ -473,6 +485,10 @@ func processCommandLineArgs(atmosConfig *schema.AtmosConfiguration, configAndSta u.LogTrace(*atmosConfig, fmt.Sprintf("Using command line argument '%s' as path to Atmos JSON Schema", configAndStacksInfo.AtmosManifestJsonSchema)) } if len(configAndStacksInfo.LogsLevel) > 0 { + if _, err := logger.ParseLogLevel(configAndStacksInfo.LogsLevel); err != nil { + return err + } + // Only set the log level if validation passes atmosConfig.Logs.Level = configAndStacksInfo.LogsLevel u.LogTrace(*atmosConfig, fmt.Sprintf("Using command line argument '%s=%s'", LogsLevelFlag, configAndStacksInfo.LogsLevel)) } diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 833885f6f..5fe37fa2f 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -20,6 +20,15 @@ const ( LogLevelWarning LogLevel = "Warning" ) +// logLevelOrder defines the order of log levels from most verbose to least verbose +var logLevelOrder = map[LogLevel]int{ + LogLevelTrace: 0, + LogLevelDebug: 1, + LogLevelInfo: 2, + LogLevelWarning: 3, + LogLevelOff: 4, +} + type Logger struct { LogLevel LogLevel File string @@ -45,18 +54,14 @@ func ParseLogLevel(logLevel string) (LogLevel, error) { return LogLevelInfo, nil } - switch LogLevel(logLevel) { // Convert logLevel to type LogLevel - case LogLevelTrace: - return LogLevelTrace, nil - case LogLevelDebug: - return LogLevelDebug, nil - case LogLevelInfo: - return LogLevelInfo, nil - case LogLevelWarning: - return LogLevelWarning, nil - default: - return LogLevelInfo, fmt.Errorf("invalid log level '%s'. Supported log levels are Trace, Debug, Info, Warning, Off", logLevel) + validLevels := []LogLevel{LogLevelTrace, LogLevelDebug, LogLevelInfo, LogLevelWarning, LogLevelOff} + for _, level := range validLevels { + if LogLevel(logLevel) == level { + return level, nil + } } + + return "", fmt.Errorf("Error: Invalid log level '%s'. Valid options are: %v", logLevel, validLevels) } func (l *Logger) log(logColor *color.Color, message string) { @@ -104,7 +109,7 @@ func (l *Logger) SetLogLevel(logLevel LogLevel) error { } func (l *Logger) Error(err error) { - if err != nil { + if err != nil && l.LogLevel != LogLevelOff { _, err2 := theme.Colors.Error.Fprintln(color.Error, err.Error()+"\n") if err2 != nil { color.Red("Error logging the error:") @@ -115,35 +120,34 @@ func (l *Logger) Error(err error) { } } +// isLevelEnabled checks if a given log level should be enabled based on the logger's current level +func (l *Logger) isLevelEnabled(level LogLevel) bool { + if l.LogLevel == LogLevelOff { + return false + } + return logLevelOrder[level] >= logLevelOrder[l.LogLevel] +} + func (l *Logger) Trace(message string) { - if l.LogLevel == LogLevelTrace { + if l.isLevelEnabled(LogLevelTrace) { l.log(theme.Colors.Info, message) } } func (l *Logger) Debug(message string) { - if l.LogLevel == LogLevelTrace || - l.LogLevel == LogLevelDebug { - + if l.isLevelEnabled(LogLevelDebug) { l.log(theme.Colors.Info, message) } } func (l *Logger) Info(message string) { - if l.LogLevel == LogLevelTrace || - l.LogLevel == LogLevelDebug || - l.LogLevel == LogLevelInfo { - - l.log(theme.Colors.Default, message) + if l.isLevelEnabled(LogLevelInfo) { + l.log(theme.Colors.Info, message) } } func (l *Logger) Warning(message string) { - if l.LogLevel == LogLevelTrace || - l.LogLevel == LogLevelDebug || - l.LogLevel == LogLevelInfo || - l.LogLevel == LogLevelWarning { - + if l.isLevelEnabled(LogLevelWarning) { l.log(theme.Colors.Warning, message) } } diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 782bc0feb..30e75f003 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -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"` + 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 { diff --git a/pkg/utils/config_utils.go b/pkg/utils/config_utils.go new file mode 100644 index 000000000..1c6c36101 --- /dev/null +++ b/pkg/utils/config_utils.go @@ -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{} + } +} diff --git a/pkg/utils/highlight_utils.go b/pkg/utils/highlight_utils.go new file mode 100644 index 000000000..11bae1345 --- /dev/null +++ b/pkg/utils/highlight_utils.go @@ -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 +} diff --git a/pkg/utils/json_utils.go b/pkg/utils/json_utils.go index 9107415a6..86ae7cba5 100644 --- a/pkg/utils/json_utils.go +++ b/pkg/utils/json_utils.go @@ -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 } diff --git a/pkg/utils/log_utils.go b/pkg/utils/log_utils.go index af3007f18..63907a6e1 100644 --- a/pkg/utils/log_utils.go +++ b/pkg/utils/log_utils.go @@ -48,7 +48,7 @@ func LogErrorAndExit(atmosConfig schema.AtmosConfiguration, err error) { // LogError logs errors to std.Error func LogError(atmosConfig schema.AtmosConfiguration, err error) { if err != nil { - _, printErr := theme.Colors.Error.Fprintln(color.Error, err.Error()+"\n") + _, printErr := theme.Colors.Error.Fprintln(color.Error, err.Error()) if printErr != nil { theme.Colors.Error.Println("Error logging the error:") theme.Colors.Error.Printf("%s\n", printErr) diff --git a/pkg/utils/yaml_utils.go b/pkg/utils/yaml_utils.go index e27c6401c..a7af8a2c9 100644 --- a/pkg/utils/yaml_utils.go +++ b/pkg/utils/yaml_utils.go @@ -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 } diff --git a/tests/fixtures/invalid-log-level/atmos.yaml b/tests/fixtures/invalid-log-level/atmos.yaml new file mode 100644 index 000000000..3af510249 --- /dev/null +++ b/tests/fixtures/invalid-log-level/atmos.yaml @@ -0,0 +1,8 @@ +logs: + level: XTrace + file: /dev/stdout + +stacks: + base_path: stacks + included_paths: + - "**/*" \ No newline at end of file diff --git a/tests/fixtures/valid-log-level/atmos.yaml b/tests/fixtures/valid-log-level/atmos.yaml new file mode 100644 index 000000000..9f4cc8c3c --- /dev/null +++ b/tests/fixtures/valid-log-level/atmos.yaml @@ -0,0 +1,8 @@ +logs: + level: Trace + file: /dev/stdout + +stacks: + base_path: stacks + included_paths: + - "**/*" \ No newline at end of file diff --git a/tests/test-cases/log-level-validation.yaml b/tests/test-cases/log-level-validation.yaml new file mode 100644 index 000000000..f7172a33b --- /dev/null +++ b/tests/test-cases/log-level-validation.yaml @@ -0,0 +1,80 @@ +tests: + - name: "Invalid Log Level in Config File" + enabled: true + description: "Test validation of invalid log level in atmos.yaml config file" + workdir: "fixtures/invalid-log-level" + command: "atmos" + args: + - terraform + - plan + - test + - -s + - test + expect: + exit_code: 1 + stderr: + - "Error: Invalid log level 'XTrace'. Valid options are: \\[Trace Debug Info Warning Off\\]" + + - name: "Invalid Log Level in Environment Variable" + enabled: true + description: "Test validation of invalid log level in ATMOS_LOGS_LEVEL env var" + workdir: "../" + command: "atmos" + args: + - terraform + - plan + - test + - -s + - test + env: + ATMOS_LOGS_LEVEL: XTrace + expect: + exit_code: 1 + stderr: + - "Error: Invalid log level 'XTrace'. Valid options are: \\[Trace Debug Info Warning Off\\]" + + - name: "Valid Log Level in Config File" + enabled: true + description: "Test validation of valid log level in atmos.yaml config file" + workdir: "fixtures/valid-log-level" + command: "atmos" + args: + - terraform + - plan + - test + - -s + - test + expect: + exit_code: 0 + + - name: "Valid Log Level in Environment Variable" + enabled: true + description: "Test validation of valid log level in ATMOS_LOGS_LEVEL env var" + workdir: "../" + command: "atmos" + args: + - terraform + - plan + - test + - -s + - test + env: + ATMOS_LOGS_LEVEL: Debug + expect: + exit_code: 0 + + - name: "Valid Log Level in Command Line Flag" + enabled: true + description: "Test validation of valid log level in --logs-level flag" + workdir: "../" + command: "atmos" + args: + - --logs-level + - Info + - terraform + - plan + - test + - -s + - test + expect: + exit_code: 0 \ No newline at end of file diff --git a/tests/test-cases/vendor-test.yaml b/tests/test-cases/vendor-test.yaml new file mode 100644 index 000000000..a1061e398 --- /dev/null +++ b/tests/test-cases/vendor-test.yaml @@ -0,0 +1,13 @@ +tests: + - name: atmos vendor pull + enabled: true + description: "" + workdir: "../" + command: "atmos" + args: + - "vendor" + - "pull" + expect: + stderr: + - "Vendoring is not configured. To set up vendoring, please see https://atmos.tools/core-concepts/vendor/" + exit_code: 1 diff --git a/website/docs/cli/configuration/markdown-styling.mdx b/website/docs/cli/configuration/markdown-styling.mdx index 5d5bd9746..dfc73b63a 100644 --- a/website/docs/cli/configuration/markdown-styling.mdx +++ b/website/docs/cli/configuration/markdown-styling.mdx @@ -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 diff --git a/website/docs/cli/configuration/terminal.mdx b/website/docs/cli/configuration/terminal.mdx new file mode 100644 index 000000000..6844e1c20 --- /dev/null +++ b/website/docs/cli/configuration/terminal.mdx @@ -0,0 +1,103 @@ +--- +title: Terminal Settings +sidebar_position: 4 +description: Configure terminal output settings including syntax highlighting +--- + +import Intro from '@site/src/components/Intro'; +import KeyPoints from '@site/src/components/KeyPoints'; +import Note from '@site/src/components/Note'; +import File from '@site/src/components/File'; + + +Atmos provides configurable terminal settings that allow you to customize the output appearance, including syntax highlighting for YAML and JSON outputs. These settings can be configured in your `atmos.yaml` configuration file. + + + +- Configure syntax highlighting for terminal output +- Customize color schemes and formatting options +- Control output pagination and line wrapping +- Set display preferences for different output formats + +## General Terminal Settings + +Configure general terminal behavior. These are also the default settings if not specified in your `atmos.yaml`: + +```yaml +settings: + terminal: + 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 in output +``` + +## Syntax Highlighting + +You can customize the syntax highlighting behavior for terminal output using the following settings: + +```yaml +settings: + terminal: + syntax_highlighting: + enabled: true # Enable/disable syntax highlighting + formatter: terminal # Output formatter + theme: dracula # Color scheme to use + line_numbers: false # Show line numbers + wrap: false # Wrap long lines +``` + +### Configuration Options + +
+
`enabled`
+
Enable or disable syntax highlighting (default: `true`)
+ +
`formatter`
+
Output formatter (default: `terminal`)
+ +
`theme`
+
+ Color scheme for syntax highlighting. Available options include: + + You can find the full list of supported themes [here](https://xyproto.github.io/splash/docs/). +
+ +
`pager`
+
Whether to use a pager specifically for syntax-highlighted output. This setting is independent of the pager setting (default: `false`)
+ +
`line_numbers`
+
Show line numbers in output (default: `false`)
+ +
`wrap`
+
Wrap long lines (default: `false`)
+
+ +### Example Usage + +The syntax highlighting is automatically applied when using commands that output YAML or JSON, such as: + +```bash +# Display config in YAML format with syntax highlighting +atmos describe config -f yaml +# Display config in JSON format with syntax highlighting +atmos describe config +``` + + +When the output is piped to another command, syntax highlighting is automatically disabled to ensure compatibility: +```bash +# Syntax highlighting is disabled when piping +atmos describe config | grep base_path +``` + + +## Supported Themes + +Atmos supports a wide range of themes for syntax highlighting. You can find the full list of supported themes [here](https://xyproto.github.io/splash/docs/). \ No newline at end of file