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';
+
+