Skip to content

Commit

Permalink
Glamorous implementation (#853)
Browse files Browse the repository at this point in the history
* colors default

* added more case colors

* reorder ui

* default renderer

* adding styles

* general styles

* workflow implementation msgs

* workflow implementation msgs

* workflow implementation msgs

* markdown start point implementation for workflow

* markdown implementation and styles improvements

* parser md implementation

* workflow file

* markdown renderer

* purple and cyan colors back

* fixes renderer and markdown

* fix pkg location

* fixes headers

* sync main

* updates for making styles dynamic set

* markdown styles docs

* fixes markdown and styles

* fix help subcommands

* dynamic size

* terminal width

* fixed colors names

* replace namecolor

* fix docs markdown

* Update website/docs/cli/configuration/markdown-styling.mdx

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>

* Update website/docs/cli/configuration/markdown-styling.mdx

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>

* explain color degradation

* fix h1 headers

* remove broken link

* format workflow file

* added safe style

* error handling for edge cases

* Update website/docs/cli/configuration/markdown-styling.mdx

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>

* update docs

* update terminal settings

* import statement

* unify settings

* remove example

* remove deprecated options atmos.yaml

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>

* updates

* update yaml and clean up

* general clean up

* update docs

* remove syntax

* clean up spaces

---------

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>
Co-authored-by: aknysh <[email protected]>
  • Loading branch information
3 people authored Jan 4, 2025
1 parent 02ec47e commit 52a3442
Show file tree
Hide file tree
Showing 16 changed files with 1,383 additions and 190 deletions.
29 changes: 29 additions & 0 deletions atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,37 @@ settings:
# deep-merged with all items in the source list.
list_merge_strategy: replace

# 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

# Markdown element styling
markdown:
document:
color: "${colors.text}"
heading:
color: "${colors.primary}"
bold: true
code_block:
color: "${colors.secondary}"
margin: 1
link:
color: "${colors.primary}"
underline: true
strong:
color: "${colors.secondary}"
bold: true
emph:
color: "${colors.muted}"
italic: true

version:
check:
enabled: true
timeout: 1000 # ms
frequency: 1h

14 changes: 12 additions & 2 deletions cmd/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ var docsCmd = &cobra.Command{

// Detect terminal width if not specified in `atmos.yaml`
// The default screen width is 120 characters, but uses maxWidth if set and greater than zero
maxWidth := atmosConfig.Settings.Docs.MaxWidth
maxWidth := atmosConfig.Settings.Terminal.MaxWidth
if maxWidth == 0 && atmosConfig.Settings.Docs.MaxWidth > 0 {
maxWidth = atmosConfig.Settings.Docs.MaxWidth
u.LogWarning(atmosConfig, "'settings.docs.max-width' is deprecated and will be removed in a future version. Please use 'settings.terminal.max_width' instead")
}
defaultWidth := 120
screenWidth := defaultWidth

Expand Down Expand Up @@ -97,7 +101,13 @@ var docsCmd = &cobra.Command{
u.LogErrorAndExit(schema.AtmosConfiguration{}, err)
}

if err := u.DisplayDocs(componentDocs, atmosConfig.Settings.Docs.Pagination); err != nil {
usePager := atmosConfig.Settings.Terminal.Pager
if !usePager && atmosConfig.Settings.Docs.Pagination {
usePager = 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 {
u.LogErrorAndExit(schema.AtmosConfiguration{}, fmt.Errorf("failed to display documentation: %w", err))
}

Expand Down
20 changes: 20 additions & 0 deletions cmd/markdown/workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Examples:

– Use interactive UI

$ atmos workflow

– Execute a workflow

$ atmos workflow <workflow-name> --file <file>

– Execute with stack override

$ atmos workflow <workflow-name> --file <file> --stack <stack>

– Resume from specific step

$ atmos workflow <workflow-name> --file <file> --from-step <step>

For more information, refer to the **docs**:
https://atmos.tools/cli/commands/workflow/
180 changes: 170 additions & 10 deletions cmd/workflow.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,200 @@
package cmd

import (
_ "embed"
"fmt"
"os"
"strings"

"github.com/spf13/cobra"

e "github.com/cloudposse/atmos/internal/exec"
termwriter "github.com/cloudposse/atmos/internal/tui/templates/term"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/ui/markdown"
u "github.com/cloudposse/atmos/pkg/utils"
)

//go:embed markdown/workflow.md
var workflowMarkdown string

// ErrorMessage represents a structured error message
type ErrorMessage struct {
Title string
Details string
Suggestion string
}

// renderError renders an error message using the markdown renderer
func renderError(msg ErrorMessage) error {
atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
if err != nil {
return fmt.Errorf("failed to initialize atmos config: %w", err)
}

termWriter := termwriter.NewResponsiveWriter(os.Stdout)
screenWidth := termWriter.(*termwriter.TerminalWriter).GetWidth()

if atmosConfig.Settings.Docs.MaxWidth > 0 {
screenWidth = uint(min(atmosConfig.Settings.Docs.MaxWidth, int(screenWidth)))
}

renderer, err := markdown.NewRenderer(
markdown.WithWidth(screenWidth),
)
if err != nil {
return fmt.Errorf("failed to create markdown renderer: %w", err)
}

rendered, err := renderer.RenderError(msg.Title, msg.Details, msg.Suggestion)
if err != nil {
return fmt.Errorf("failed to render error message: %w", err)
}

fmt.Print(rendered)
return nil
}

// getMarkdownSection returns a section from the markdown file
func getMarkdownSection(title string) (details, suggestion string) {
sections := markdown.ParseMarkdownSections(workflowMarkdown)
if section, ok := sections[title]; ok {
parts := markdown.SplitMarkdownContent(section)
if len(parts) >= 2 {
return parts[0], parts[1]
}
return section, ""
}
return "", ""
}

// workflowCmd executes a workflow
var workflowCmd = &cobra.Command{
Use: "workflow",
Short: "Execute a workflow",
Long: `This command executes a workflow: atmos workflow <name> -f <file>`,
Long: `This command executes a workflow: atmos workflow <name> --file <file>`,
Example: "atmos workflow\n" +
"atmos workflow <name> -f <file>\n" +
"atmos workflow <name> -f <file> -s <stack>\n" +
"atmos workflow <name> -f <file> --from-step <step-name>\n\n" +
"atmos workflow <name> --file <file>\n" +
"atmos workflow <name> --file <file> --stack <stack>\n" +
"atmos workflow <name> --file <file> --from-step <step-name>\n\n" +
"To resume the workflow from this step, run:\n" +
"atmos workflow deploy-infra -f workflow1 --from-step deploy-vpc\n\n" +
"atmos workflow deploy-infra --file workflow1 --from-step deploy-vpc\n\n" +
"For more details refer to https://atmos.tools/cli/commands/workflow/",
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
Run: func(cmd *cobra.Command, args []string) {
// If no arguments are provided, start the workflow UI
if len(args) == 0 {
err := e.ExecuteWorkflowCmd(cmd, args)
if err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, err)
}
return
}

if args[0] == "help" {
if err := cmd.Help(); err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, err)
}
return
}

// Get the --file flag value
workflowFile, _ := cmd.Flags().GetString("file")

// If no file is provided, show invalid command error with usage information
if workflowFile == "" {
// Get atmos configuration
atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
if err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, fmt.Errorf("failed to initialize atmos config: %w", err))
}

// Create a terminal writer to get the optimal width
termWriter := termwriter.NewResponsiveWriter(os.Stdout)
screenWidth := termWriter.(*termwriter.TerminalWriter).GetWidth()

if atmosConfig.Settings.Docs.MaxWidth > 0 {
screenWidth = uint(min(atmosConfig.Settings.Docs.MaxWidth, int(screenWidth)))
}

renderer, err := markdown.NewRenderer(
markdown.WithWidth(screenWidth),
)
if err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, fmt.Errorf("failed to create markdown renderer: %w", err))
}

// Generate the error message dynamically using H1 styling
errorMsg := fmt.Sprintf("# Invalid Command\n\nThe command `atmos workflow %s` is not valid.\n\n", args[0])
content := errorMsg + workflowMarkdown
rendered, err := renderer.Render(content)
if err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, fmt.Errorf("failed to render markdown: %w", err))
}

// Remove duplicate URLs and format output
lines := strings.Split(rendered, "\n")
var result []string
seenURL := false

for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.Contains(trimmed, "https://") {
if !seenURL {
seenURL = true
result = append(result, line)
}
} else if strings.HasPrefix(trimmed, "$") {
result = append(result, " "+strings.TrimSpace(line))
} else if trimmed != "" {
result = append(result, line)
}
}

fmt.Print("\n" + strings.Join(result, "\n") + "\n\n")
os.Exit(1)
}

// Execute the workflow command
err := e.ExecuteWorkflowCmd(cmd, args)
if err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, err)
// Format common error messages
if strings.Contains(err.Error(), "does not exist") {
details, suggestion := getMarkdownSection("Workflow File Not Found")
err := renderError(ErrorMessage{
Title: "Workflow File Not Found",
Details: details,
Suggestion: suggestion,
})
if err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, err)
}
} else if strings.Contains(err.Error(), "does not have the") {
details, suggestion := getMarkdownSection("Invalid Workflow")
err := renderError(ErrorMessage{
Title: "Invalid Workflow",
Details: details,
Suggestion: suggestion,
})
if err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, err)
}
} else {
// For other errors, use the standard error handler
u.LogErrorAndExit(schema.AtmosConfiguration{}, err)
}
return
}
},
}

func init() {
workflowCmd.DisableFlagParsing = false
workflowCmd.PersistentFlags().StringP("file", "f", "", "atmos workflow <name> -f <file>")
workflowCmd.PersistentFlags().Bool("dry-run", false, "atmos workflow <name> -f <file> --dry-run")
workflowCmd.PersistentFlags().StringP("stack", "s", "", "atmos workflow <name> -f <file> -s <stack>")
workflowCmd.PersistentFlags().String("from-step", "", "atmos workflow <name> -f <file> --from-step <step-name>")
workflowCmd.PersistentFlags().StringP("file", "f", "", "atmos workflow <name> --file <file>")
workflowCmd.PersistentFlags().Bool("dry-run", false, "atmos workflow <name> --file <file> --dry-run")
workflowCmd.PersistentFlags().StringP("stack", "s", "", "atmos workflow <name> --file <file> --stack <stack>")
workflowCmd.PersistentFlags().String("from-step", "", "atmos workflow <name> --file <file> --from-step <step-name>")

RootCmd.AddCommand(workflowCmd)
}
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/charmbracelet/log v0.4.0
github.com/elewis787/boa v0.1.2
github.com/fatih/color v1.18.0
github.com/go-git/go-git/v5 v5.13.0
github.com/go-git/go-git/v5 v5.13.1
github.com/gofrs/flock v0.12.1
github.com/google/go-containerregistry v0.20.2
github.com/google/go-github/v59 v59.0.0
Expand All @@ -39,6 +39,7 @@ require (
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/go-wordwrap v1.0.1
github.com/mitchellh/mapstructure v1.5.0
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a
github.com/open-policy-agent/opa v1.0.0
github.com/otiai10/copy v1.14.0
github.com/pkg/errors v0.9.1
Expand Down Expand Up @@ -113,7 +114,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/docker/cli v27.1.1+incompatible // indirect
Expand All @@ -127,7 +128,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.0 // indirect
github.com/go-git/go-billy/v5 v5.6.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
Expand Down Expand Up @@ -194,7 +195,6 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand Down Expand Up @@ -513,8 +513,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKBAG3Klr9cn7N+LcYc82AZ2S7+cA=
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug=
github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/elewis787/boa v0.1.2 h1:xNKWJ9X2MWbLSLLOA31N4l1Jdec9FZSkbTvXy3C8rw4=
github.com/elewis787/boa v0.1.2/go.mod h1:EFDKuz/bYgQAKJQBnfHmB9i+bBzsaZJyyoSmOz6eBZI=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
Expand Down Expand Up @@ -562,12 +562,12 @@ github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E=
github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw=
github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Expand Down
Loading

0 comments on commit 52a3442

Please sign in to comment.