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 #907

Merged
merged 8 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,16 @@ settings:
# If the source and destination lists have the same length, all items in the destination lists are
# deep-merged with all items in the source list.
list_merge_strategy: replace
terminal:
syntax_highlighting:
enabled: true
lexer: yaml # Default lexer for the content
formatter: terminal # Output formatter (e.g., terminal, html)
style: dracula # Highlighting style
pager: true # Enable pager
options:
line_numbers: true # Display line numbers
wrap: false # Wrap long lines

# Terminal settings for displaying content
terminal:
Expand Down
101 changes: 99 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ var (
Frequency: "daily",
},
},
Settings: &schema.AtmosSettings{
Terminal: &schema.TerminalSettings{
SyntaxHighlighting: &schema.SyntaxHighlightingSettings{
Enabled: true,
Lexer: "yaml",
Formatter: "terminal",
Style: "dracula",
Pager: false,
Options: &schema.SyntaxHighlightOptions{
LineNumbers: false,
Wrap: false,
},
},
},
},
Cerebrovinny marked this conversation as resolved.
Show resolved Hide resolved
}
)

Expand All @@ -111,6 +126,17 @@ func InitCliConfig(configAndStacksInfo schema.ConfigAndStacksInfo, processStacks
v.SetConfigType("yaml")
v.SetTypeByDefaultValue(true)

// Load default configuration first
defaultConfigJSON, err := json.Marshal(defaultCliConfig)
if err != nil {
return atmosConfig, err
}

defaultReader := bytes.NewReader(defaultConfigJSON)
if err := v.ReadConfig(defaultReader); err != nil {
return atmosConfig, err
}

// Default configuration values
v.SetDefault("components.helmfile.use_eks", true)
v.SetDefault("components.terraform.append_user_agent", fmt.Sprintf("Atmos/%s (Cloud Posse; +https://atmos.tools)", version.Version))
Expand Down Expand Up @@ -263,6 +289,31 @@ func InitCliConfig(configAndStacksInfo schema.ConfigAndStacksInfo, processStacks
atmosConfig.Components.Terraform.AppendUserAgent = fmt.Sprintf("Atmos/%s (Cloud Posse; +https://atmos.tools)", version.Version)
}

// Initialize settings with defaults if not set
if atmosConfig.Settings == nil {
atmosConfig.Settings = defaultCliConfig.Settings
} else {
// Only initialize nil fields with defaults
if atmosConfig.Settings.Terminal == nil {
atmosConfig.Settings.Terminal = defaultCliConfig.Settings.Terminal
} else if atmosConfig.Settings.Terminal.SyntaxHighlighting == nil {
atmosConfig.Settings.Terminal.SyntaxHighlighting = defaultCliConfig.Settings.Terminal.SyntaxHighlighting
} else {
// Update settings from viper
atmosConfig.Settings.Terminal.SyntaxHighlighting.Enabled = v.GetBool("settings.terminal.syntax_highlighting.enabled")
atmosConfig.Settings.Terminal.SyntaxHighlighting.Lexer = v.GetString("settings.terminal.syntax_highlighting.lexer")
atmosConfig.Settings.Terminal.SyntaxHighlighting.Formatter = v.GetString("settings.terminal.syntax_highlighting.formatter")
atmosConfig.Settings.Terminal.SyntaxHighlighting.Style = v.GetString("settings.terminal.syntax_highlighting.style")
atmosConfig.Settings.Terminal.SyntaxHighlighting.Pager = v.GetBool("settings.terminal.syntax_highlighting.pager")

if atmosConfig.Settings.Terminal.SyntaxHighlighting.Options == nil {
atmosConfig.Settings.Terminal.SyntaxHighlighting.Options = &schema.SyntaxHighlightOptions{}
}
atmosConfig.Settings.Terminal.SyntaxHighlighting.Options.LineNumbers = v.GetBool("settings.terminal.syntax_highlighting.options.line_numbers")
atmosConfig.Settings.Terminal.SyntaxHighlighting.Options.Wrap = v.GetBool("settings.terminal.syntax_highlighting.options.wrap")
}
}

// Check config
err = checkConfig(atmosConfig)
if err != nil {
Expand Down Expand Up @@ -375,10 +426,56 @@ func processConfigFile(
}
}(reader)

err = v.MergeConfig(reader)
if err != nil {
// Create a new viper instance for this config file
fileViper := viper.New()
fileViper.SetConfigType("yaml")

// Read the config file
if err := fileViper.ReadConfig(reader); err != nil {
return false, err
}

// Get all settings from the file
settings := fileViper.AllSettings()

// Merge settings into the main viper instance
for key, value := range settings {
if key == "settings" {
// Handle settings section separately to preserve nested values
if settingsMap, ok := value.(map[string]interface{}); ok {
if terminalMap, ok := settingsMap["terminal"].(map[string]interface{}); ok {
if syntaxMap, ok := terminalMap["syntax_highlighting"].(map[string]interface{}); ok {
// Set each field individually to preserve nested values
if enabled, ok := syntaxMap["enabled"].(bool); ok {
v.Set("settings.terminal.syntax_highlighting.enabled", enabled)
}
if lexer, ok := syntaxMap["lexer"].(string); ok {
v.Set("settings.terminal.syntax_highlighting.lexer", lexer)
}
if formatter, ok := syntaxMap["formatter"].(string); ok {
v.Set("settings.terminal.syntax_highlighting.formatter", formatter)
}
if style, ok := syntaxMap["style"].(string); ok {
v.Set("settings.terminal.syntax_highlighting.style", style)
}
if pager, ok := syntaxMap["pager"].(bool); ok {
v.Set("settings.terminal.syntax_highlighting.pager", pager)
}
if options, ok := syntaxMap["options"].(map[string]interface{}); ok {
if lineNumbers, ok := options["line_numbers"].(bool); ok {
v.Set("settings.terminal.syntax_highlighting.options.line_numbers", lineNumbers)
}
if wrap, ok := options["wrap"].(bool); ok {
v.Set("settings.terminal.syntax_highlighting.options.wrap", wrap)
}
}
}
}
}
} else {
v.Set(key, value)
}
}

return true, nil
}
54 changes: 36 additions & 18 deletions pkg/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type AtmosConfiguration struct {
Integrations Integrations `yaml:"integrations,omitempty" json:"integrations,omitempty" mapstructure:"integrations"`
Schemas Schemas `yaml:"schemas,omitempty" json:"schemas,omitempty" mapstructure:"schemas"`
Templates Templates `yaml:"templates,omitempty" json:"templates,omitempty" mapstructure:"templates"`
Settings AtmosSettings `yaml:"settings,omitempty" json:"settings,omitempty" mapstructure:"settings"`
Settings *AtmosSettings `yaml:"settings,omitempty" json:"settings,omitempty" mapstructure:"settings"`
StoresConfig store.StoresConfig `yaml:"stores,omitempty" json:"stores,omitempty" mapstructure:"stores"`
Vendor Vendor `yaml:"vendor,omitempty" json:"vendor,omitempty" mapstructure:"vendor"`
Initialized bool `yaml:"initialized" json:"initialized" mapstructure:"initialized"`
Expand All @@ -37,19 +37,43 @@ type AtmosConfiguration struct {
Stores store.StoreRegistry `yaml:"stores_registry,omitempty" json:"stores_registry,omitempty" mapstructure:"stores_registry"`
}

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"`
type AtmosSettings struct {
ListMergeStrategy string `yaml:"list_merge_strategy" json:"list_merge_strategy" mapstructure:"list_merge_strategy"`
Terminal *TerminalSettings `yaml:"terminal,omitempty" json:"terminal,omitempty" mapstructure:"terminal"`
Docs Docs `yaml:"docs,omitempty" json:"docs,omitempty" mapstructure:"docs"`
Markdown MarkdownSettings `yaml:"markdown,omitempty" json:"markdown,omitempty" mapstructure:"markdown"`
}

type AtmosSettings struct {
ListMergeStrategy string `yaml:"list_merge_strategy" json:"list_merge_strategy" mapstructure:"list_merge_strategy"`
Terminal Terminal `yaml:"terminal,omitempty" json:"terminal,omitempty" mapstructure:"terminal"`
Docs Docs `yaml:"docs,omitempty" json:"docs,omitempty" mapstructure:"docs"`
Markdown MarkdownSettings `yaml:"markdown,omitempty" json:"markdown,omitempty" mapstructure:"markdown"`
type TerminalSettings struct {
MaxWidth int `yaml:"max_width,omitempty" json:"max_width,omitempty" mapstructure:"max_width"`
Pager bool `yaml:"pager,omitempty" json:"pager,omitempty" mapstructure:"pager"`
Timestamps bool `yaml:"timestamps,omitempty" json:"timestamps,omitempty" mapstructure:"timestamps"`
Colors bool `yaml:"colors,omitempty" json:"colors,omitempty" mapstructure:"colors"`
Unicode bool `yaml:"unicode,omitempty" json:"unicode,omitempty" mapstructure:"unicode"`
SyntaxHighlighting *SyntaxHighlightingSettings `yaml:"syntax_highlighting,omitempty" json:"syntax_highlighting,omitempty" mapstructure:"syntax_highlighting"`
}

type SyntaxHighlightOptions struct {
LineNumbers bool `yaml:"line_numbers" json:"line_numbers" mapstructure:"line_numbers"`
Wrap bool `yaml:"wrap" json:"wrap" mapstructure:"wrap"`
}

type SyntaxHighlightingSettings 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"`
Style string `yaml:"style" json:"style" mapstructure:"style"`
Pager bool `yaml:"pager" json:"pager" mapstructure:"pager"`
Options *SyntaxHighlightOptions `yaml:"options,omitempty" json:"options,omitempty" mapstructure:"options"`
}

type Settings struct {
DependsOn DependsOn `yaml:"depends_on,omitempty" json:"depends_on,omitempty" mapstructure:"depends_on"`
Spacelift SettingsSpacelift `yaml:"spacelift,omitempty" json:"spacelift,omitempty" mapstructure:"spacelift"`
Templates Templates `yaml:"templates,omitempty" json:"templates,omitempty" mapstructure:"templates"`
ListMergeStrategy string `yaml:"list_merge_strategy,omitempty" json:"list_merge_strategy,omitempty"`
Terminal *TerminalSettings `yaml:"terminal,omitempty" json:"terminal,omitempty" mapstructure:"terminal"`
Docs *Docs `yaml:"docs,omitempty" json:"docs,omitempty" mapstructure:"docs"`
}

type Docs struct {
Expand Down Expand Up @@ -541,12 +565,6 @@ type Dependent struct {

type SettingsSpacelift AtmosSectionMapType

type Settings struct {
DependsOn DependsOn `yaml:"depends_on,omitempty" json:"depends_on,omitempty" mapstructure:"depends_on"`
Spacelift SettingsSpacelift `yaml:"spacelift,omitempty" json:"spacelift,omitempty" mapstructure:"spacelift"`
Templates Templates `yaml:"templates,omitempty" json:"templates,omitempty" mapstructure:"templates"`
}

// ConfigSourcesStackDependency defines schema for sources of config sections
type ConfigSourcesStackDependency struct {
StackFile string `yaml:"stack_file" json:"stack_file" mapstructure:"stack_file"`
Expand Down
160 changes: 160 additions & 0 deletions pkg/utils/highlight_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package utils

import (
"bytes"
"io"
"os"

"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/quick"
"github.com/alecthomas/chroma/styles"
"golang.org/x/term"

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

// DefaultHighlightSettings returns the default syntax highlighting settings
func DefaultHighlightSettings() *schema.SyntaxHighlightingSettings {
return &schema.SyntaxHighlightingSettings{
Enabled: true,
Lexer: "yaml",
Formatter: "terminal",
Style: "dracula",
Options: &schema.SyntaxHighlightOptions{
LineNumbers: false,
Wrap: false,
},
}
}

// GetHighlightSettings returns the syntax highlighting settings from the config or defaults
func GetHighlightSettings(config schema.AtmosConfiguration) *schema.SyntaxHighlightingSettings {
defaults := DefaultHighlightSettings()

if config.Settings == nil {
return defaults
}

if config.Settings.Terminal == nil {
config.Settings.Terminal = &schema.TerminalSettings{
SyntaxHighlighting: defaults,
}
}

if config.Settings.Terminal.SyntaxHighlighting == nil {
config.Settings.Terminal.SyntaxHighlighting = defaults
return defaults
}

settings := config.Settings.Terminal.SyntaxHighlighting

// Apply defaults for any unset fields
if !settings.Enabled {
settings.Enabled = defaults.Enabled
}
if settings.Lexer == "" {
settings.Lexer = defaults.Lexer
}
if settings.Formatter == "" {
settings.Formatter = defaults.Formatter
}
if settings.Style == "" {
settings.Style = defaults.Style
}
if settings.Options == nil {
settings.Options = defaults.Options
}

return settings
}

// HighlightCode highlights the given code using chroma with the specified lexer and style
func HighlightCode(code string, lexerName string, style string) (string, error) {
if !term.IsTerminal(int(os.Stdout.Fd())) {
return code, nil
}

var buf bytes.Buffer
err := quick.Highlight(&buf, code, lexerName, "terminal", style)
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) (string, error) {
if !term.IsTerminal(int(os.Stdout.Fd())) {
return code, nil
}

settings := GetHighlightSettings(config)
if !settings.Enabled {
return code, nil
}

// Get lexer
lexer := lexers.Get(settings.Lexer)
if lexer == nil {
lexer = lexers.Fallback
}

// Get style
s := styles.Get(settings.Style)
if s == nil {
s = styles.Fallback
}

// Get formatter
var formatter chroma.Formatter
if settings.Options != nil && settings.Options.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
}

// NewHighlightWriter creates a new HighlightWriter
func NewHighlightWriter(w io.Writer, config schema.AtmosConfiguration) *HighlightWriter {
return &HighlightWriter{
config: config,
writer: w,
}
}

// Write implements io.Writer
func (h *HighlightWriter) Write(p []byte) (n int, err error) {
highlighted, err := HighlightCodeWithConfig(string(p), h.config)
if err != nil {
return 0, err
}
return h.writer.Write([]byte(highlighted))
}
Loading
Loading