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/log" "github.com/schollz/progressbar/v3" ) var BoldStyle = 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) { table := table.New(). Border(lipgloss.ThickBorder()). BorderStyle( lipgloss.NewStyle(). Foreground(lipgloss.Color("63")), ) 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") return table, nil } width, _, err := term.GetSize(0) if err != nil { return nil, err } if width-10 < 79 { // NOTE(d1): maintain standard minimum width table.Width(79) } else { // NOTE(d1): tests show that this produces stable border drawing table.Width(width - 10) } return table, nil } // 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.OptionFullWidth(), 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]) }