package formatter import ( "bytes" "encoding/json" "fmt" "os" "strings" "time" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" "github.com/docker/go-units" "golang.org/x/term" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/log" "github.com/schollz/progressbar/v3" ) var BoldStyle = lipgloss.NewStyle(). Bold(true) var BoldUnderlineStyle = lipgloss.NewStyle(). Bold(true). Underline(true) func ShortenID(str string) string { return str[:12] } func SmallSHA(hash string) string { return hash[:8] } // RemoveSha remove image sha from a string that are added in some docker outputs func RemoveSha(str string) string { return strings.Split(str, "@")[0] } // HumanDuration from docker/cli RunningFor() to be accessible outside of the class func HumanDuration(timestamp int64) string { date := time.Unix(timestamp, 0) now := time.Now().UTC() return units.HumanDuration(now.Sub(date)) + " ago" } // CreateTable prepares a table layout for output. func CreateTable() (*table.Table, error) { var ( renderer = lipgloss.NewRenderer(os.Stdout) headerStyle = renderer.NewStyle().Bold(true).Align(lipgloss.Center) cellStyle = renderer.NewStyle().Padding(0, 1) borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) ) table := table.New(). Border(lipgloss.ThickBorder()). BorderStyle(borderStyle). StyleFunc(func(row, col int) lipgloss.Style { var style lipgloss.Style switch { case row == table.HeaderRow: return headerStyle default: style = cellStyle } return style }) return table, nil } func PrintTable(t *table.Table) error { if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" { // NOTE(d1): no width limits for CI testing since we test against outputs log.Debug("detected ABRA_CI=1") fmt.Println(t) return nil } tWidth, _ := lipgloss.Size(t.String()) width, _, err := term.GetSize(0) if err != nil { return err } if tWidth > width { t.Width(width - 10) } fmt.Println(t) return nil } // horizontal is a JoinHorizontal helper function. func horizontal(left, mid, right string) string { return lipgloss.JoinHorizontal(lipgloss.Right, left, mid, right) } func CreateOverview(header string, rows [][]string) string { var borderStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.ThickBorder()). Padding(0, 1, 0, 1). BorderForeground(lipgloss.Color("63")) var headerStyle = lipgloss.NewStyle(). Underline(true). Bold(true). PaddingBottom(1) var leftStyle = lipgloss.NewStyle() var rightStyle = lipgloss.NewStyle() var longest int for _, row := range rows { if len(row[0]) > longest { longest = len(row[0]) } } var renderedRows []string for _, row := range rows { if len(row) < 2 { continue } if len(row) > 2 { panic("CreateOverview: only accepts rows of len == 2") } lenOffset := 4 if len(row[0]) < longest { lenOffset += longest - len(row[0]) } offset := "" for range lenOffset { offset = offset + " " } rendered := horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1])) if row[1] == "---" { rendered = horizontal( leftStyle. Bold(true). Underline(true). PaddingTop(1). Render(row[0]), offset, rightStyle.Render(""), ) } renderedRows = append(renderedRows, rendered) } body := strings.Builder{} body.WriteString( borderStyle.Render( lipgloss.JoinVertical( lipgloss.Center, headerStyle.Render(header), lipgloss.JoinVertical( lipgloss.Left, renderedRows..., ), ), ), ) return body.String() } // ToJSON converts a lipgloss.Table to JSON representation. It's not a robust // implementation and mainly caters for our current use case which is basically // a bunch of strings. See https://github.com/charmbracelet/lipgloss/issues/335 // for the real thing (hopefully). func ToJSON(headers []string, rows [][]string) (string, error) { var buff bytes.Buffer buff.Write([]byte("[")) for idx, row := range rows { payload := make(map[string]string) for idx, header := range headers { payload[strings.ToLower(header)] = row[idx] } serialized, err := json.Marshal(payload) if err != nil { return "", err } buff.Write(serialized) if idx < (len(rows) - 1) { buff.Write([]byte(",")) } } buff.Write([]byte("]")) return buff.String(), nil } // CreateProgressbar generates a progress bar func CreateProgressbar(length int, title string) *progressbar.ProgressBar { return progressbar.NewOptions( length, progressbar.OptionClearOnFinish(), progressbar.OptionSetPredictTime(false), progressbar.OptionShowCount(), progressbar.OptionSetDescription(title), ) } // StripTagMeta strips front-matter image tag data that we don't need for parsing. func StripTagMeta(image string) string { originalImage := image if strings.Contains(image, "docker.io") { image = strings.Split(image, "/")[1] } if strings.Contains(image, "library") { image = strings.Split(image, "/")[1] } if originalImage != image { log.Debugf("stripped %s to %s for parsing", originalImage, image) } return image } // ByteCountSI presents a human friendly representation of a byte count. See // https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format. func ByteCountSI(b uint64) string { const unit = 1000 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := uint64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) } // BoldDirtyDefault ensures a dirty modifier is rendered in bold. func BoldDirtyDefault(v string) string { if strings.HasSuffix(v, config.DIRTY_DEFAULT) { vBold := BoldStyle.Render(config.DIRTY_DEFAULT) v = strings.Replace(v, config.DIRTY_DEFAULT, vBold, 1) } return v } // AddDirtyMarker adds the dirty marker to a version string. func AddDirtyMarker(v string) string { return fmt.Sprintf("%s%s", v, config.DIRTY_DEFAULT) }